engine: complete C ABI facade (liboakengine oakengine_* surface)
The full pure-C facade used by the app: node/project/timeline/viewer/ undo/task/events/serializer/playback/preview/renderer/gizmo/color/ audio/footage/proxy/encoding/exporter/config/disk/ipc/plugin/worker families, plus undo-group semantics, display renderer handles, NodeFactory accessors, and per-family pure-C engine tests.
This commit is contained in:
+90
-1
@@ -51,6 +51,13 @@ add_library(oakengine SHARED
|
||||
${OLIVE_RESOURCES}
|
||||
)
|
||||
|
||||
# macOS: hides the render worker's dock icon; called from the worker main in
|
||||
# src/capi/worker.cpp (declared there as a plain C++ symbol).
|
||||
if (APPLE)
|
||||
target_sources(oakengine PRIVATE src/worker_dockicon_mac.mm)
|
||||
target_link_libraries(oakengine PRIVATE "-framework Cocoa")
|
||||
endif ()
|
||||
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(pluginSupport)
|
||||
|
||||
@@ -61,7 +68,7 @@ set_target_properties(oakengine PROPERTIES
|
||||
)
|
||||
|
||||
# Consumers resolve engine headers ("node/...", "render/...", "coreengine.h",
|
||||
# "ui/icons/icons.h", "tool/tool.h") from the engine root, and the public C
|
||||
# "tool/tool.h") from the engine root, and the public C
|
||||
# API ("oakengine/ipc.h") from include/. The library itself builds against its
|
||||
# internal implementation headers under src/oliveimpl, included with an
|
||||
# "oliveimpl/"-prefixed path resolved from src/ (mirrors the src/oliveimpl
|
||||
@@ -227,13 +234,47 @@ if (BUILD_TESTS)
|
||||
|
||||
make_oakengine_test(oakengine_ipc_test)
|
||||
|
||||
make_oakengine_test(oakengine_worker_test)
|
||||
|
||||
make_oakengine_test(oakengine_init_test)
|
||||
|
||||
make_oakengine_test(oakengine_app_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}"
|
||||
)
|
||||
|
||||
make_oakengine_test(oakengine_task_test)
|
||||
# The task test creates temporary projects and imports non-existent files,
|
||||
# so it only needs the headless engine services.
|
||||
|
||||
make_oakengine_test(oakengine_config_test)
|
||||
# The config test exercises the QSettings-backed key/value store through
|
||||
# the C ABI; it is headless and does not touch the disk cache.
|
||||
|
||||
make_oakengine_test(oakengine_audio_test)
|
||||
# The audio test exercises the AudioManager instance lifecycle, device
|
||||
# get/set round-trips and the output_params_changed event. It is headless
|
||||
# and only needs PortAudio initialization.
|
||||
|
||||
make_oakengine_test(oakengine_disk_test)
|
||||
# The disk test exercises the DiskManager instance lifecycle, default cache
|
||||
# path queries, cache clearing, settings handler dispatch and default path
|
||||
# mutation. It is headless and only touches temporary directories.
|
||||
|
||||
make_oakengine_test(oakengine_proxy_test)
|
||||
# The proxy test exercises proxy parameter defaults, state string
|
||||
# round-trips and the ProxyManager singleton lifecycle. It is headless.
|
||||
|
||||
make_oakengine_test(oakengine_lut_test)
|
||||
# The LUT test exercises directory/file list queries and the
|
||||
# set_directories round-trip. It is headless.
|
||||
|
||||
make_oakengine_test(oakengine_serializer_test)
|
||||
# The serializer test exercises compressed-project detection, clipboard
|
||||
# create/free/copy and empty-node copy. It is headless.
|
||||
|
||||
make_oakengine_test(oakengine_renderer_test)
|
||||
# The renderer test builds sequence content through the engine C++ API
|
||||
# (allowed for engine-internal tests) and probes the dynamic render
|
||||
@@ -275,6 +316,15 @@ if (BUILD_TESTS)
|
||||
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
make_oakengine_test(oakengine_events_test)
|
||||
target_compile_definitions(oakengine_events_test PRIVATE
|
||||
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
make_oakengine_test(oakengine_encoding_test)
|
||||
|
||||
make_oakengine_test(oakengine_color_test)
|
||||
|
||||
make_oakengine_test(oakengine_export_test)
|
||||
# The export test builds sequence content through the engine C++ API and
|
||||
# probes the dynamic render backend like oakengine_renderer_test does.
|
||||
@@ -303,6 +353,8 @@ if (BUILD_TESTS)
|
||||
endif ()
|
||||
|
||||
make_oakengine_test(oakengine_node_test)
|
||||
|
||||
make_oakengine_test(oakengine_nodevalue_test)
|
||||
target_compile_definitions(oakengine_node_test PRIVATE
|
||||
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
@@ -312,6 +364,16 @@ if (BUILD_TESTS)
|
||||
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
make_oakengine_test(oakengine_viewer_test)
|
||||
target_compile_definitions(oakengine_viewer_test PRIVATE
|
||||
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
make_oakengine_test(oakengine_traverse_test)
|
||||
target_compile_definitions(oakengine_traverse_test PRIVATE
|
||||
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
make_oakengine_test(oakengine_preview_test)
|
||||
# The preview test needs audio rendering (RenderManager + workers).
|
||||
target_include_directories(oakengine_preview_test PRIVATE
|
||||
@@ -362,4 +424,31 @@ if (BUILD_TESTS)
|
||||
if (TARGET olive-render-worker)
|
||||
add_dependencies(oakengine_playback_test olive-render-worker)
|
||||
endif ()
|
||||
|
||||
make_oakengine_test(oakengine_sync_test)
|
||||
# The sync test renders the clips' audio through the worker pool (same
|
||||
# needs as oakengine_playback_test).
|
||||
target_include_directories(oakengine_sync_test PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/third_party/openfx/include
|
||||
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
|
||||
${OLIVE_INCLUDE_DIRS}
|
||||
)
|
||||
target_compile_definitions(oakengine_sync_test PRIVATE
|
||||
${OLIVE_DEFINITIONS}
|
||||
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
target_compile_options(oakengine_sync_test PRIVATE
|
||||
${OLIVE_COMPILE_OPTIONS}
|
||||
)
|
||||
if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
|
||||
target_compile_definitions(oakengine_sync_test PRIVATE
|
||||
OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
|
||||
add_dependencies(oakengine_sync_test oakgl)
|
||||
if (TARGET oakvulkan)
|
||||
add_dependencies(oakengine_sync_test oakvulkan)
|
||||
endif ()
|
||||
endif ()
|
||||
if (TARGET olive-render-worker)
|
||||
add_dependencies(oakengine_sync_test olive-render-worker)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
/***
|
||||
|
||||
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_APP_H
|
||||
#define OAKENGINE_APP_H
|
||||
|
||||
#include "export.h"
|
||||
#include "footage.h"
|
||||
#include "init.h"
|
||||
#include "project.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file app.h
|
||||
* @brief C ABI for the application-level engine state (EngineCore facade)
|
||||
*
|
||||
* This family exposes the process-wide application state the editor UI needs
|
||||
* from the engine: the CoreParams-driven startup, the open/active project,
|
||||
* the recent-projects list, the global tool/snapping/timecode settings, the
|
||||
* status bar and the UI handler hooks the engine calls when it needs user
|
||||
* interaction (image-sequence confirmation, footage relink, project save /
|
||||
* close, main window layout restore, OTIO import).
|
||||
*
|
||||
* It wraps olive::EngineCore so that the UI layer no longer derives from or
|
||||
* links against that C++ class. The engine emits its change notifications
|
||||
* through the OakEngineAppCallbacks function pointers (registered with
|
||||
* oakengine_app_set_callbacks()) instead of Qt signals.
|
||||
*
|
||||
* Conventions (matching oakengine/project.h):
|
||||
* - Booleans are int (1/0).
|
||||
* - Return codes: 0 (OAKENGINE_OK) on success, negative OAKENGINE_E_* on
|
||||
* failure. Functions documented as returning a value return
|
||||
* OAKENGINE_E_INVALID when no application core exists.
|
||||
* - 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.
|
||||
* - Enum values mirror the engine enums (olive::Tool::Item,
|
||||
* olive::Tool::AddableObject, olive::core::Timecode::Display) and are
|
||||
* passed as plain int; the numeric values are identical.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Application run modes (mirrors olive::EngineCore::CoreParams::RunMode).
|
||||
*/
|
||||
#define OAKENGINE_APP_RUN_NORMAL 0 /**< Normal GUI run. */
|
||||
#define OAKENGINE_APP_RUN_HEADLESS_EXPORT 1 /**< Export without GUI. */
|
||||
#define OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE 2 /**< Pre-cache without GUI. */
|
||||
|
||||
/**
|
||||
* @brief Startup parameters for oakengine_app_create().
|
||||
*
|
||||
* Strings may be NULL (treated as empty). The struct is copied by
|
||||
* oakengine_app_create(); the pointed-to strings are only read during the
|
||||
* call.
|
||||
*/
|
||||
typedef struct OakEngineAppParams {
|
||||
int run_mode; /**< OAKENGINE_APP_RUN_* value. */
|
||||
int fullscreen; /**< Start the main window fullscreen (bool). */
|
||||
const char *startup_project; /**< Project file to open on startup, or NULL. */
|
||||
const char *startup_language; /**< .qm file overriding the language, or NULL. */
|
||||
int crash_on_startup; /**< Trigger a manual crash shortly after start (bool). */
|
||||
} OakEngineAppParams;
|
||||
|
||||
/**
|
||||
* @brief UI handler and notification callback set.
|
||||
*
|
||||
* Any field may be NULL. A NULL handler makes the engine fall back to its
|
||||
* headless default (accept the import, close without prompting, skip the
|
||||
* file write); a NULL notification simply drops the event.
|
||||
*
|
||||
* The callbacks are invoked synchronously on the thread that triggered the
|
||||
* engine call (usually the main thread). `userdata` is passed back verbatim.
|
||||
*
|
||||
* `load_layout` receives a `const olive::SerializedLayoutInfo *` (engine
|
||||
* data structure, only valid during the call). `otio_import` receives an
|
||||
* array of borrowed olive::Sequence pointers as OakEngineSequence handles.
|
||||
*/
|
||||
typedef struct OakEngineAppCallbacks {
|
||||
void *userdata;
|
||||
|
||||
/* UI handlers (engine asks the application) */
|
||||
int (*confirm_image_sequence)(const char *filename, void *userdata);
|
||||
int (*relink_footage)(OakEngineFootage **footage, int count,
|
||||
void *userdata);
|
||||
void (*save_project)(const char *override_filename, void *userdata);
|
||||
int (*close_project)(void *userdata);
|
||||
void (*load_layout)(const void *layout, void *userdata);
|
||||
int (*otio_import)(OakEngineSequence **sequences, int count,
|
||||
void *userdata);
|
||||
|
||||
/* Notifications (engine informs the application) */
|
||||
void (*status_message_show)(const char *message, int timeout,
|
||||
void *userdata);
|
||||
void (*status_message_clear)(void *userdata);
|
||||
void (*cache_full_warning)(void *userdata);
|
||||
void (*active_project_changed)(OakEngineProject *project, void *userdata);
|
||||
void (*tool_changed)(int tool, void *userdata);
|
||||
void (*addable_object_changed)(int object, void *userdata);
|
||||
void (*snapping_changed)(int snapping, void *userdata);
|
||||
void (*timecode_display_changed)(int display, void *userdata);
|
||||
void (*open_recent_list_changed)(void *userdata);
|
||||
void (*color_picker_enabled)(int enabled, void *userdata);
|
||||
} OakEngineAppCallbacks;
|
||||
|
||||
/**
|
||||
* @brief Create the application engine core with the given startup params.
|
||||
*
|
||||
* `params` may be NULL for defaults (normal run, no startup project). Only
|
||||
* one application core may exist per process: if one already exists (either
|
||||
* from an earlier oakengine_app_create() or from the EngineCore shell that
|
||||
* oakengine_init() creates), OAKENGINE_E_STATE is returned.
|
||||
*
|
||||
* The core is never destroyed; it backs the process-wide engine singleton.
|
||||
*
|
||||
* @return OAKENGINE_OK on success, OAKENGINE_E_STATE if a core exists.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_create(const OakEngineAppParams *params);
|
||||
|
||||
/**
|
||||
* @brief Start the engine services for the application (config, locale,
|
||||
* managers, autorecovery timer, recent projects list).
|
||||
*
|
||||
* @return OAKENGINE_OK on success, OAKENGINE_E_STATE if no core exists or
|
||||
* the application core was already started.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_start(void);
|
||||
|
||||
/**
|
||||
* @brief Stop the engine services started by oakengine_app_start().
|
||||
*
|
||||
* @return OAKENGINE_OK on success, OAKENGINE_E_STATE if the application
|
||||
* core was not started.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Register the UI handler/notification callback set.
|
||||
*
|
||||
* The struct is copied; NULL clears all callbacks and restores the headless
|
||||
* default behavior.
|
||||
*
|
||||
* @return OAKENGINE_OK.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_app_set_callbacks(const OakEngineAppCallbacks *callbacks);
|
||||
|
||||
/**
|
||||
* @brief Startup parameter accessors (valid once a core exists).
|
||||
*
|
||||
* oakengine_app_run_mode() returns an OAKENGINE_APP_RUN_* value,
|
||||
* oakengine_app_fullscreen() a boolean; both return OAKENGINE_E_INVALID when
|
||||
* no core exists. oakengine_app_startup_project() uses the buf/size
|
||||
* convention.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_run_mode(void);
|
||||
OAKENGINE_API int oakengine_app_fullscreen(void);
|
||||
OAKENGINE_API int oakengine_app_startup_project(char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Process-wide undo stack as an opaque pointer (an
|
||||
* olive::UndoStack *). Returns NULL when no core exists.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_app_undo_stack(void);
|
||||
|
||||
/**
|
||||
* @brief Current tool as an olive::Tool::Item value (int).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_tool(void);
|
||||
|
||||
/**
|
||||
* @brief Set the current tool. Valid values are 0 <= tool < Tool::k_count.
|
||||
* Emits the tool_changed notification.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_tool(int tool);
|
||||
|
||||
/**
|
||||
* @brief Currently selected addable object (olive::Tool::AddableObject).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_addable_object(void);
|
||||
|
||||
/**
|
||||
* @brief Set the addable object. Valid values are 0 <= object <
|
||||
* Tool::k_addable_count. Emits addable_object_changed.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_addable_object(int object);
|
||||
|
||||
/**
|
||||
* @brief Currently selected transition id (buf/size convention).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_selected_transition(char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Set the selected transition id (NULL clears it).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_selected_transition(const char *id);
|
||||
|
||||
/**
|
||||
* @brief Current snapping setting (boolean).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_snapping(void);
|
||||
|
||||
/**
|
||||
* @brief Set snapping. Emits snapping_changed.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_snapping(int enabled);
|
||||
|
||||
/**
|
||||
* @brief Current timecode display mode (olive::core::Timecode::Display).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_timecode_display(void);
|
||||
|
||||
/**
|
||||
* @brief Set the timecode display mode (0 <= display <= 4). Emits
|
||||
* timecode_display_changed.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_timecode_display(int display);
|
||||
|
||||
/**
|
||||
* @brief Number of entries in the recently opened/saved projects list.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_recent_projects_count(void);
|
||||
|
||||
/**
|
||||
* @brief Path of the recent-project entry at `index` (buf/size convention).
|
||||
*
|
||||
* @return the string length, or OAKENGINE_E_NOT_FOUND for an invalid index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_recent_project_at(int index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Remove the recent-project entry at `index`. Emits
|
||||
* open_recent_list_changed.
|
||||
*
|
||||
* @return OAKENGINE_OK, or OAKENGINE_E_NOT_FOUND for an invalid index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_remove_recent_project(int index);
|
||||
|
||||
/**
|
||||
* @brief Clear the recent projects list. Emits open_recent_list_changed.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_clear_recent_projects(void);
|
||||
|
||||
/**
|
||||
* @brief Show a message in the status bar (delivered through the
|
||||
* status_message_show callback).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_show_status_message(const char *message,
|
||||
int timeout);
|
||||
|
||||
/**
|
||||
* @brief Clear the status bar (delivered through the status_message_clear
|
||||
* callback).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_clear_status_message(void);
|
||||
|
||||
/**
|
||||
* @brief Change the current language.
|
||||
*
|
||||
* @return 1 if a translation for `locale` was found and installed, 0 if
|
||||
* not, OAKENGINE_E_INVALID for NULL or when no core exists.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_language(const char *locale);
|
||||
|
||||
/**
|
||||
* @brief Set how frequently an autorecovery is saved (minutes).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_autorecovery_interval(int minutes);
|
||||
|
||||
/**
|
||||
* @brief Globally enable/disable decoding from proxy media.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_use_proxy_media(int enabled);
|
||||
|
||||
/**
|
||||
* @brief Add/remove a pixel-sampling user. Emits color_picker_enabled when
|
||||
* the user count crosses 0.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_request_pixel_sampling(int enable);
|
||||
|
||||
/**
|
||||
* @brief Debug "magic" flag accessors.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_magic(int enabled);
|
||||
OAKENGINE_API int oakengine_app_is_magic_enabled(void);
|
||||
|
||||
/**
|
||||
* @brief Copy a string to the system clipboard.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_copy_to_clipboard(const char *text);
|
||||
|
||||
/**
|
||||
* @brief Paste a string from the system clipboard (buf/size convention).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_paste_from_clipboard(char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief File filter for footage import dialogs (buf/size convention).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_footage_file_dialog_filter(char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Whether `path` has an extension allowed for footage import.
|
||||
*
|
||||
* @return 1/0, or OAKENGINE_E_INVALID for NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_is_footage_extension_allowed(const char *path);
|
||||
|
||||
/**
|
||||
* @brief Create a new sequence named appropriately for `project`.
|
||||
*
|
||||
* `name_format` is a QString::arg() pattern (e.g. "Sequence %1"); NULL uses
|
||||
* the default "Sequence %1". The returned handle is owned by the caller
|
||||
* (it is not yet added to the project). Returns NULL on invalid input.
|
||||
*/
|
||||
OAKENGINE_API OakEngineSequence *
|
||||
oakengine_app_create_sequence(OakEngineProject *project,
|
||||
const char *name_format);
|
||||
|
||||
/**
|
||||
* @brief Path of the autorecovery index file (buf/size convention).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_auto_recovery_index_filename(char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Currently open project (borrowed handle, may be NULL).
|
||||
*/
|
||||
OAKENGINE_API OakEngineProject *oakengine_app_open_project(void);
|
||||
|
||||
/**
|
||||
* @brief Close the current project (through the close_project handler) and
|
||||
* open a new empty one.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_create_new_project(void);
|
||||
|
||||
/**
|
||||
* @brief Open an already-loaded project, closing the current one first.
|
||||
* Pushes it to the recent list when `add_to_recents` is set and the project
|
||||
* has a filename.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_add_open_project(OakEngineProject *project,
|
||||
int add_to_recents);
|
||||
|
||||
/**
|
||||
* @brief Adopt the project loaded by a project-load task (an olive::Task *
|
||||
* as an opaque pointer).
|
||||
*
|
||||
* @return 1 if the project was opened, 0 if the load was cancelled or the
|
||||
* footage validation was rejected, OAKENGINE_E_INVALID for NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_add_open_project_from_task(void *task,
|
||||
int add_to_recents);
|
||||
|
||||
/**
|
||||
* @brief Adopt an autorecovery project loaded by a project-load task (an
|
||||
* olive::Task * as an opaque pointer).
|
||||
*
|
||||
* @return 1 on success, 0 otherwise, OAKENGINE_E_INVALID for NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_add_recovery_project_from_task(void *task);
|
||||
|
||||
/**
|
||||
* @brief Update engine state after `project` was successfully saved (recent
|
||||
* list, modified flag, unrecovered list).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_on_project_saved(OakEngineProject *project);
|
||||
|
||||
/**
|
||||
* @brief Set the active (open) project. Emits active_project_changed.
|
||||
* `project` may be NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_active_project(OakEngineProject *project);
|
||||
|
||||
/**
|
||||
* @brief Convenience wrapper: set just the confirm-image-sequence handler
|
||||
* (same as setting cb.confirm_image_sequence in oakengine_app_set_callbacks).
|
||||
* Replaces both fn and userdata.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_confirm_image_sequence_handler(
|
||||
int (*fn)(const char *filename, void *userdata), void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Convenience wrapper: set just the relink handler.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_relink_handler(
|
||||
int (*fn)(OakEngineFootage **footage, int count, void *userdata),
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Convenience wrapper: set just the save-project handler.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_save_project_handler(
|
||||
void (*fn)(const char *override_filename, void *userdata), void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Convenience wrapper: set just the close-project handler.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_close_project_handler(
|
||||
int (*fn)(void *userdata), void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Convenience wrapper: set just the load-layout handler.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_load_layout_handler(
|
||||
void (*fn)(const void *layout, void *userdata), void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Alias for oakengine_app_auto_recovery_index_filename().
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_get_auto_recovery_index_filename(char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Alias for oakengine_app_remove_recent_project().
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_remove_recently_opened_project(int index);
|
||||
|
||||
/**
|
||||
* @brief void*-based overload of oakengine_app_on_project_saved() for use
|
||||
* from app code that holds a opaque QObject pointer.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_on_project_saved_vp(void *project);
|
||||
|
||||
/**
|
||||
* @brief void*-based overload of oakengine_app_set_active_project().
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_set_active_project_vp(void *project);
|
||||
|
||||
/**
|
||||
* @brief void*-based overload of oakengine_app_add_open_project().
|
||||
*/
|
||||
OAKENGINE_API int oakengine_app_add_open_project_vp(void *project,
|
||||
int add_to_recents);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_APP_H */
|
||||
@@ -0,0 +1,330 @@
|
||||
/***
|
||||
|
||||
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_AUDIO_H
|
||||
#define OAKENGINE_AUDIO_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
#include "encoding.h"
|
||||
#include "init.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file audio.h
|
||||
* @brief C ABI for the engine's audio I/O singleton (olive::AudioManager)
|
||||
*
|
||||
* A thin facade over AudioManager's instance lifecycle, input/output device
|
||||
* selection, output buffer management and recording stop control. The
|
||||
* AudioManager handle returned by oakengine_audio_manager_handle() is a
|
||||
* borrowed opaque pointer intended only for event subscription
|
||||
* (OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED); it is not a general
|
||||
* purpose object handle and must not be freed.
|
||||
*
|
||||
* Conventions match the other facade families:
|
||||
* - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
|
||||
* - Device indices are PortAudio PaDeviceIndex values (int64_t across the
|
||||
* boundary); paNoDevice is -1.
|
||||
* - String output uses the buf/size convention (error_buf for
|
||||
* oakengine_audio_push_to_output).
|
||||
*/
|
||||
|
||||
typedef struct OakAudioParams OakAudioParams;
|
||||
|
||||
/**
|
||||
* @brief Create the AudioManager singleton.
|
||||
*
|
||||
* Safe to call when the instance already exists (no-op). Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_FAILED.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_create_instance(void);
|
||||
|
||||
/**
|
||||
* @brief Destroy the AudioManager singleton.
|
||||
*
|
||||
* Safe to call when no instance exists (no-op). Returns OAKENGINE_OK.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_destroy_instance(void);
|
||||
|
||||
/**
|
||||
* @brief Borrowed handle to the AudioManager singleton, or NULL if none.
|
||||
*
|
||||
* Intended only for subscribing to
|
||||
* OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED. The pointer is owned
|
||||
* by the engine and becomes NULL after oakengine_audio_destroy_instance().
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_audio_manager_handle(void);
|
||||
|
||||
/**
|
||||
* @brief Current output device index (paNoDevice = -1 when none).
|
||||
*
|
||||
* Returns the current value from the AudioManager singleton, or paNoDevice if
|
||||
* no instance exists.
|
||||
*/
|
||||
OAKENGINE_API int64_t oakengine_audio_get_output_device(void);
|
||||
|
||||
/**
|
||||
* @brief Set the output device index.
|
||||
*
|
||||
* Changing the device may emit output_params_changed. Returns OAKENGINE_OK or
|
||||
* OAKENGINE_E_FAILED.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_set_output_device(int64_t device);
|
||||
|
||||
/**
|
||||
* @brief Current input device index (paNoDevice = -1 when none).
|
||||
*/
|
||||
OAKENGINE_API int64_t oakengine_audio_get_input_device(void);
|
||||
|
||||
/**
|
||||
* @brief Set the input device index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_set_input_device(int64_t device);
|
||||
|
||||
/**
|
||||
* @brief Re-initialize PortAudio and refresh the device lists.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_hard_reset(void);
|
||||
|
||||
/**
|
||||
* @brief Clear any buffered output samples.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_clear_buffered_output(void);
|
||||
|
||||
/**
|
||||
* @brief Push a packed sample buffer to the current output device.
|
||||
*
|
||||
* `params` is an owned or borrowed OakAudioParams handle describing the
|
||||
* sample data. `samples` points to `samples_size` bytes of interleaved audio
|
||||
* data in the format described by `params`. On failure a human-readable
|
||||
* message is written into `error_buf` (up to `error_buf_size` bytes including
|
||||
* the terminating NUL) and OAKENGINE_E_FAILED is returned.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_push_to_output(const OakAudioParams *params,
|
||||
const char *samples,
|
||||
int64_t samples_size,
|
||||
char *error_buf,
|
||||
int error_buf_size);
|
||||
|
||||
/**
|
||||
* @brief Stop an active recording session.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_stop_recording(void);
|
||||
|
||||
/**
|
||||
* @brief Stop audio output.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_stop_output(void);
|
||||
|
||||
/**
|
||||
* @brief Restart the output clock at zero for a new playback run.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_reset_output_clock(void);
|
||||
|
||||
/**
|
||||
* @brief Set the output notify interval in bytes.
|
||||
*
|
||||
* After this many bytes of audio have been consumed by the output device,
|
||||
* the AudioManager emits output_notify (which translates to the
|
||||
* OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY event for C subscribers).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_set_output_notify_interval(int64_t bytes);
|
||||
|
||||
/**
|
||||
* @brief Start audio recording.
|
||||
*
|
||||
* Takes ownership of `params`: the handle is destroyed when the recording
|
||||
* ends. On failure a human-readable message is written into `error_buf`
|
||||
* (up to `error_buf_size` bytes including the terminating NUL).
|
||||
*
|
||||
* @return OAKENGINE_OK on success, OAKENGINE_E_FAILED on error.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_start_recording(
|
||||
OakEngineEncodingParams *params, char *error_buf, int error_buf_size);
|
||||
|
||||
/* ---- Audio synchronization (R6 P1.3) ------------------------------------ */
|
||||
|
||||
/** @brief Result of envelope-offset correlation. */
|
||||
typedef struct oak_audio_waveform_offset {
|
||||
int64_t offset_samples;
|
||||
double confidence;
|
||||
/** 1 if the offset is usable, 0 otherwise. */
|
||||
int valid;
|
||||
} oak_audio_waveform_offset;
|
||||
|
||||
/** @brief Result of rate+offset correlation. */
|
||||
typedef struct oak_audio_waveform_stretch_offset {
|
||||
double rate;
|
||||
int64_t offset_samples;
|
||||
double confidence;
|
||||
/** 1 if the result is usable, 0 otherwise. */
|
||||
int valid;
|
||||
} oak_audio_waveform_stretch_offset;
|
||||
|
||||
/**
|
||||
* @brief Estimate the sample offset between two RMS envelopes.
|
||||
*
|
||||
* `reference_valid`/`candidate_valid` may be NULL to mean "all windows valid";
|
||||
* if non-NULL their lengths must equal `reference_len`/`candidate_len`.
|
||||
*
|
||||
* @return OAKENGINE_OK with `out` filled, or OAKENGINE_E_INVALID.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_estimate_envelope_offset(
|
||||
const double *reference, int reference_len,
|
||||
const double *candidate, int candidate_len,
|
||||
const bool *reference_valid, int reference_valid_len,
|
||||
const bool *candidate_valid, int candidate_valid_len,
|
||||
uint64_t window_samples, int64_t max_offset_windows,
|
||||
oak_audio_waveform_offset *out);
|
||||
|
||||
/**
|
||||
* @brief Estimate a playback-rate change plus offset aligning candidate to
|
||||
* reference.
|
||||
*
|
||||
* See AudioWaveformSync::estimate_stretch_and_offset().
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_estimate_stretch_and_offset(
|
||||
const double *reference, int reference_len,
|
||||
const double *candidate, int candidate_len,
|
||||
const bool *reference_valid, int reference_valid_len,
|
||||
const bool *candidate_valid, int candidate_valid_len,
|
||||
uint64_t window_samples, int64_t max_offset_windows,
|
||||
double min_rate, double max_rate, double rate_step,
|
||||
oak_audio_waveform_stretch_offset *out);
|
||||
|
||||
/** @brief Source-clip description for source-time synchronization. */
|
||||
typedef struct oak_audio_sync_source_clip {
|
||||
/** Source start time as a Rational num/den pair. */
|
||||
int64_t source_start_time_num;
|
||||
int64_t source_start_time_den;
|
||||
/** Media in-point as a Rational num/den pair. */
|
||||
int64_t media_in_num;
|
||||
int64_t media_in_den;
|
||||
/** 1 if source_start_time is meaningful, 0 otherwise. */
|
||||
int has_source_start_time;
|
||||
} oak_audio_sync_source_clip;
|
||||
|
||||
/** @brief Timeline placement result from AudioSynchronizer. */
|
||||
typedef struct oak_audio_sync_placement {
|
||||
/** Timeline in-point as a Rational num/den pair. */
|
||||
int64_t timeline_in_num;
|
||||
int64_t timeline_in_den;
|
||||
/** 1 if the placement is usable, 0 otherwise. */
|
||||
int valid;
|
||||
} oak_audio_sync_placement;
|
||||
|
||||
/**
|
||||
* @brief Compute a candidate clip's timeline placement from source timecodes.
|
||||
*
|
||||
* `reference_timeline_in` is the reference clip's timeline in-point as a
|
||||
* Rational num/den pair.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_sync_place_by_source_time(
|
||||
const oak_audio_sync_source_clip *reference,
|
||||
const oak_audio_sync_source_clip *candidate,
|
||||
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
|
||||
oak_audio_sync_placement *out);
|
||||
|
||||
/**
|
||||
* @brief Compute a candidate clip's timeline placement from a waveform offset.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_sync_place_by_waveform_offset(
|
||||
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
|
||||
int64_t candidate_offset_samples, int sample_rate,
|
||||
oak_audio_sync_placement *out);
|
||||
|
||||
/* ---- Audio format processor (R6 P5) ------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Opaque audio format converter (olive::AudioProcessor).
|
||||
*
|
||||
* Converts planar float samples from one format to a packed output format,
|
||||
* optionally applying tempo (speed) scaling. Used by the viewer to feed the
|
||||
* audio output device. Create with oakengine_audio_processor_create() and
|
||||
* destroy with oakengine_audio_processor_free().
|
||||
*/
|
||||
typedef struct OakEngineAudioProcessor OakEngineAudioProcessor;
|
||||
|
||||
/**
|
||||
* @brief Create an audio processor with no open graph.
|
||||
*
|
||||
* Returns NULL on allocation failure.
|
||||
*/
|
||||
OAKENGINE_API OakEngineAudioProcessor *oakengine_audio_processor_create(void);
|
||||
|
||||
/**
|
||||
* @brief Destroy the processor, closing any open graph. Safe to call with
|
||||
* NULL.
|
||||
*/
|
||||
OAKENGINE_API void oakengine_audio_processor_free(OakEngineAudioProcessor *p);
|
||||
|
||||
/**
|
||||
* @brief Open the conversion graph.
|
||||
*
|
||||
* `from` describes the planar float input format and `to` the packed output
|
||||
* format (both borrowed handles, copied internally). `tempo` is the playback
|
||||
* speed (1.0 = normal). The processor must not already be open. Returns
|
||||
* OAKENGINE_OK, OAKENGINE_E_INVALID, or OAKENGINE_E_FAILED.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_processor_open(OakEngineAudioProcessor *p,
|
||||
const OakAudioParams *from, const OakAudioParams *to, double tempo);
|
||||
|
||||
/**
|
||||
* @brief Close the conversion graph. Safe to call when not open or with
|
||||
* NULL.
|
||||
*/
|
||||
OAKENGINE_API void oakengine_audio_processor_close(OakEngineAudioProcessor *p);
|
||||
|
||||
/**
|
||||
* @brief 1 if the processor has an open graph, 0 otherwise (or NULL).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_processor_is_open(OakEngineAudioProcessor *p);
|
||||
|
||||
/**
|
||||
* @brief Convert planar float samples to the packed output format.
|
||||
*
|
||||
* `in` is an array of per-channel float pointers (channel count as given to
|
||||
* open()); `nb_in_samples` is the number of frames. On success (>= 0),
|
||||
* `*out_data` points to the packed output bytes owned by `p` (valid until the
|
||||
* next convert/close/free) and `*out_size` holds the byte count, which may be
|
||||
* 0 when the tempo buffer absorbed the block. Returns a negative error code
|
||||
* on failure.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_processor_convert(OakEngineAudioProcessor *p,
|
||||
float **in, int nb_in_samples, const void **out_data, int *out_size);
|
||||
|
||||
/**
|
||||
* @brief Output (packed) parameters as a new OakAudioParams handle.
|
||||
*
|
||||
* The caller owns the result and must free it with oakcore_audioparams_free().
|
||||
* Returns NULL if `p` is NULL or not open.
|
||||
*/
|
||||
OAKENGINE_API OakAudioParams *oakengine_audio_processor_output_params(
|
||||
OakEngineAudioProcessor *p);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_AUDIO_H */
|
||||
@@ -0,0 +1,297 @@
|
||||
/***
|
||||
|
||||
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_COLOR_H
|
||||
#define OAKENGINE_COLOR_H
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "project.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file color.h
|
||||
* @brief C ABI for color management (the olive::ColorManager /
|
||||
* ColorTransform / ColorProcessor surface)
|
||||
*
|
||||
* Covers everything the application's display and color-picker paths need
|
||||
* without importing an engine C++ symbol:
|
||||
*
|
||||
* - OakEngineColorManager: borrowed handle to a project's color manager
|
||||
* (olive::ColorManager). Obtain it with
|
||||
* oakengine_color_manager_from_project(); like the other borrowed
|
||||
* handles it is just the engine pointer reinterpreted and its lifetime
|
||||
* follows the project. All list queries use the index + buf/size
|
||||
* string pattern (the return value of a string getter is the would-be
|
||||
* length excluding the NUL, so buf == NULL queries the size).
|
||||
*
|
||||
* - oak_color_transform: POD mirror of olive::ColorTransform. `output`
|
||||
* is the colorspace name when `is_display` is 0, otherwise the display
|
||||
* device name with `view`/`look` selecting the display transform. NULL
|
||||
* strings mean "unset" (the empty QString).
|
||||
*
|
||||
* - OakEngineColorProcessor: owned handle wrapping an OCIO-backed
|
||||
* olive::ColorProcessorPtr. Free with
|
||||
* oakengine_color_processor_free(). Color conversion is per-color
|
||||
* (double RGBA in/out); the frame-level GPU path goes through
|
||||
* ColorTransformJob on the engine side.
|
||||
*
|
||||
* - OakEngineColorConfig: owned handle to a standalone OCIO config (the
|
||||
* project properties dialog lists the colorspaces of a config file
|
||||
* before applying it).
|
||||
*
|
||||
* Change notifications (config reloads, reference space changes) are
|
||||
* delivered through the event family: subscribe with
|
||||
* OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED /
|
||||
* OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED (oakengine/events.h).
|
||||
*
|
||||
* Error model: functions that can hit an OCIO failure report the reason
|
||||
* through oakengine_color_last_error() (thread-local, buf/size
|
||||
* convention). List/query functions never fail on a valid handle.
|
||||
*/
|
||||
|
||||
/** @brief Borrowed color manager handle (olive::ColorManager). */
|
||||
typedef struct OakEngineColorManager OakEngineColorManager;
|
||||
|
||||
/** @brief Owned color processor handle; free with oakengine_color_processor_free(). */
|
||||
typedef struct OakEngineColorProcessor OakEngineColorProcessor;
|
||||
|
||||
/** @brief Owned standalone OCIO config handle; free with oakengine_color_config_free(). */
|
||||
typedef struct OakEngineColorConfig OakEngineColorConfig;
|
||||
|
||||
/** @brief Processor direction: input -> output (olive k_normal). */
|
||||
#define OAKENGINE_COLOR_PROCESSOR_NORMAL 0
|
||||
/** @brief Processor direction: output -> input (olive k_inverse). */
|
||||
#define OAKENGINE_COLOR_PROCESSOR_INVERSE 1
|
||||
|
||||
/**
|
||||
* @brief POD mirror of olive::ColorTransform. Strings are UTF-8; NULL is
|
||||
* the unset/empty value.
|
||||
*/
|
||||
typedef struct oak_color_transform {
|
||||
int is_display; /**< 0: `output` is a colorspace; 1: display/view/look. */
|
||||
const char *output; /**< Colorspace name, or display device when is_display. */
|
||||
const char *view; /**< Display view (is_display only). */
|
||||
const char *look; /**< Display look (is_display only). */
|
||||
} oak_color_transform;
|
||||
|
||||
/**
|
||||
* @brief Human-readable reason of the last failed color call on this
|
||||
* thread (buf/size convention). Empty when the last call succeeded.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_color_last_error(char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief The project's color manager (borrowed; NULL for a NULL project or
|
||||
* a project without one).
|
||||
*/
|
||||
OAKENGINE_API OakEngineColorManager *
|
||||
oakengine_color_manager_from_project(OakEngineProject *project);
|
||||
|
||||
/** @brief Current OCIO config filename of the manager (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_manager_get_config_filename(
|
||||
const OakEngineColorManager *mgr, char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Point the manager at a different OCIO config file
|
||||
* (ColorManager::set_config_filename()). OAKENGINE_E_INVALID for NULL
|
||||
* args. OCIO load failures surface lazily through the list queries.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_color_manager_set_config_filename(
|
||||
OakEngineColorManager *mgr, const char *filename);
|
||||
|
||||
/** @brief Number of colorspaces in the manager's active config. */
|
||||
OAKENGINE_API int oakengine_color_manager_colorspace_count(
|
||||
const OakEngineColorManager *mgr);
|
||||
|
||||
/** @brief Name of the `index`-th colorspace (buf/size); OAKENGINE_E_INVALID out of range. */
|
||||
OAKENGINE_API int oakengine_color_manager_colorspace_at(
|
||||
const OakEngineColorManager *mgr, int index, char *buf, int buf_size);
|
||||
|
||||
/** @brief Number of display devices in the active config. */
|
||||
OAKENGINE_API int oakengine_color_manager_display_count(
|
||||
const OakEngineColorManager *mgr);
|
||||
|
||||
/** @brief Name of the `index`-th display device (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_manager_display_at(
|
||||
const OakEngineColorManager *mgr, int index, char *buf, int buf_size);
|
||||
|
||||
/** @brief Number of views available on `display` (NULL/empty = active display). */
|
||||
OAKENGINE_API int oakengine_color_manager_view_count(
|
||||
const OakEngineColorManager *mgr, const char *display);
|
||||
|
||||
/** @brief Name of the `index`-th view on `display` (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_manager_view_at(
|
||||
const OakEngineColorManager *mgr, const char *display, int index,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/** @brief Number of looks in the active config. */
|
||||
OAKENGINE_API int oakengine_color_manager_look_count(
|
||||
const OakEngineColorManager *mgr);
|
||||
|
||||
/** @brief Name of the `index`-th look (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_manager_look_at(
|
||||
const OakEngineColorManager *mgr, int index, char *buf, int buf_size);
|
||||
|
||||
/** @brief The config's default display device (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_manager_default_display(
|
||||
const OakEngineColorManager *mgr, char *buf, int buf_size);
|
||||
|
||||
/** @brief The config's default view for `display` (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_manager_default_view(
|
||||
const OakEngineColorManager *mgr, const char *display, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/** @brief The project's default input colorspace (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_manager_default_input_color_space(
|
||||
const OakEngineColorManager *mgr, char *buf, int buf_size);
|
||||
|
||||
/** @brief Set the project's default input colorspace. */
|
||||
OAKENGINE_API int oakengine_color_manager_set_default_input_color_space(
|
||||
OakEngineColorManager *mgr, const char *colorspace);
|
||||
|
||||
/** @brief The config's reference (scene-linear) colorspace (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_manager_reference_color_space(
|
||||
const OakEngineColorManager *mgr, char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief The config's default luma coefficients written to `rgb` (exactly
|
||||
* 3 doubles; ColorManager::get_default_luma_coefs()). OAKENGINE_E_INVALID
|
||||
* for NULL args.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_color_manager_default_luma_coefs(
|
||||
const OakEngineColorManager *mgr, double *rgb);
|
||||
|
||||
/**
|
||||
* @brief Resolve `name` to a colorspace of the active config
|
||||
* (ColorManager::get_compliant_color_space(QString); buf/size). Unknown
|
||||
* names resolve to the default input colorspace.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_color_manager_compliant_color_space(
|
||||
const OakEngineColorManager *mgr, const char *name, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Resolve a transform to one the active config supports
|
||||
* (ColorManager::get_compliant_color_space(ColorTransform, force_display)).
|
||||
*
|
||||
* The resolved transform is written into the output buffers; any output
|
||||
* pointer may be NULL. Buffers that are too small truncate (NUL-terminated
|
||||
* when size > 0). `out_is_display` receives the resolved kind.
|
||||
*
|
||||
* @return OAKENGINE_OK, or OAKENGINE_E_INVALID for NULL mgr/in.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_color_manager_compliant_transform(
|
||||
const OakEngineColorManager *mgr, const oak_color_transform *in,
|
||||
int force_display, int *out_is_display, char *out_output,
|
||||
int output_size, char *out_view, int view_size, char *out_look,
|
||||
int look_size);
|
||||
|
||||
/**
|
||||
* @brief Load the engine's built-in default OCIO config (owned handle).
|
||||
* NULL on failure (see oakengine_color_last_error()).
|
||||
*/
|
||||
OAKENGINE_API OakEngineColorConfig *oakengine_color_config_load_default(void);
|
||||
|
||||
/**
|
||||
* @brief Load an OCIO config from `filename` (owned handle). NULL on
|
||||
* failure (see oakengine_color_last_error()).
|
||||
*/
|
||||
OAKENGINE_API OakEngineColorConfig *
|
||||
oakengine_color_config_load_file(const char *filename);
|
||||
|
||||
/** @brief Release a config handle (NULL-safe no-op). */
|
||||
OAKENGINE_API void oakengine_color_config_free(OakEngineColorConfig *config);
|
||||
|
||||
/** @brief Number of colorspaces in the config. */
|
||||
OAKENGINE_API int
|
||||
oakengine_color_config_colorspace_count(const OakEngineColorConfig *config);
|
||||
|
||||
/** @brief Name of the `index`-th colorspace in the config (buf/size). */
|
||||
OAKENGINE_API int oakengine_color_config_colorspace_at(
|
||||
const OakEngineColorConfig *config, int index, char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Create a color processor converting from colorspace `input` to
|
||||
* the `dest` transform (ColorProcessor::create(); owned handle).
|
||||
*
|
||||
* `direction` is OAKENGINE_COLOR_PROCESSOR_NORMAL or
|
||||
* OAKENGINE_COLOR_PROCESSOR_INVERSE. OCIO failures are non-fatal (matching
|
||||
* the engine's C++ behavior): the handle is still returned but
|
||||
* oakengine_color_processor_is_valid() reports 0 and conversions are
|
||||
* pass-through.
|
||||
*
|
||||
* @return The handle, or NULL for NULL mgr/input/dest or an unknown
|
||||
* direction.
|
||||
*/
|
||||
OAKENGINE_API OakEngineColorProcessor *oakengine_color_processor_create(
|
||||
const OakEngineColorManager *mgr, const char *input,
|
||||
const oak_color_transform *dest, int direction);
|
||||
|
||||
/** @brief Release a processor handle (NULL-safe no-op). */
|
||||
OAKENGINE_API void oakengine_color_processor_free(OakEngineColorProcessor *proc);
|
||||
|
||||
/**
|
||||
* @brief 1 when the processor holds a valid OCIO processor
|
||||
* (ColorProcessor::get_processor() != null), 0 otherwise.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_color_processor_is_valid(const OakEngineColorProcessor *proc);
|
||||
|
||||
/**
|
||||
* @brief Convert a single RGBA color (ColorProcessor::convert_color()).
|
||||
* `in_rgba`/`out_rgba` are 4-double arrays; on an invalid processor the
|
||||
* input is copied through.
|
||||
*
|
||||
* @return OAKENGINE_OK, or OAKENGINE_E_INVALID for NULL args.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_color_processor_convert_color(
|
||||
const OakEngineColorProcessor *proc, const double *in_rgba,
|
||||
double *out_rgba);
|
||||
|
||||
/**
|
||||
* @brief The OCIO cache id of the processor (ColorProcessor::id();
|
||||
* buf/size). Used by display paths to invalidate cached conversions.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_color_processor_id(
|
||||
const OakEngineColorProcessor *proc, char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Attach a processor to an engine ColorTransformJob
|
||||
* (ColorTransformJob::set_color_processor()).
|
||||
*
|
||||
* Transitional bridge for the display/scopes GPU path until the blit
|
||||
* family covers ColorTransformJob: `job` is an
|
||||
* olive::ColorTransformJob* the caller owns, passed as void* to keep the
|
||||
* C++ type out of the ABI.
|
||||
*
|
||||
* @return OAKENGINE_OK, or OAKENGINE_E_INVALID for a NULL job.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_color_transform_job_set_processor(
|
||||
void *job, const OakEngineColorProcessor *proc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_COLOR_H */
|
||||
@@ -0,0 +1,100 @@
|
||||
/***
|
||||
|
||||
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_CONFIG_H
|
||||
#define OAKENGINE_CONFIG_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "init.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file config.h
|
||||
* @brief C ABI for the engine configuration store (olive::Config).
|
||||
*
|
||||
* A thin facade over the QSettings-backed key/value store used by the editor
|
||||
* for persistent preferences. Only the types actually used by the UI are
|
||||
* exposed (string/int); the engine keeps ownership of the singleton.
|
||||
*/
|
||||
|
||||
typedef void (*oakengine_config_error_fn)(const char *title,
|
||||
const char *message,
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Load configuration from disk (Config::load).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_config_load(void);
|
||||
|
||||
/**
|
||||
* @brief Save configuration to disk (Config::save).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_config_save(void);
|
||||
|
||||
/**
|
||||
* @brief Read a string value (buf/size convention).
|
||||
*
|
||||
* @return the string length on success, 0 when the key is missing or empty,
|
||||
* or a negative OAKENGINE_E_* code on error.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_config_get_string(const char *key, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Write a string value.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_config_set_string(const char *key,
|
||||
const char *value);
|
||||
|
||||
/**
|
||||
* @brief Read an integer value. Returns `default_value` when the key is
|
||||
* missing or not convertible to int.
|
||||
*/
|
||||
OAKENGINE_API int64_t oakengine_config_get_int(const char *key,
|
||||
int64_t default_value);
|
||||
|
||||
/**
|
||||
* @brief Write an integer value.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_config_set_int(const char *key, int64_t value);
|
||||
|
||||
/**
|
||||
* @brief Register a callback for configuration errors (e.g. disk write
|
||||
* failures). Passing NULL clears the handler.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_config_set_error_handler(
|
||||
oakengine_config_error_fn fn, void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Report an error through the registered handler. If no handler is
|
||||
* set the error is logged and discarded.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_config_report_error(const char *title,
|
||||
const char *message);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_CONFIG_H */
|
||||
@@ -0,0 +1,154 @@
|
||||
/***
|
||||
|
||||
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_DISK_H
|
||||
#define OAKENGINE_DISK_H
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "project.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file disk.h
|
||||
* @brief C ABI for the engine's disk cache singleton (olive::DiskManager)
|
||||
*
|
||||
* A thin facade over DiskManager's instance lifecycle, default/custom cache
|
||||
* path management, cache clearing, settings dialog dispatch and project
|
||||
* invalidation. The opaque folder handle returned by
|
||||
* oakengine_disk_get_open_folder() is a borrowed pointer to the engine's
|
||||
* internal DiskCacheFolder for that path; it must not be freed and becomes
|
||||
* invalid when the DiskManager instance is destroyed.
|
||||
*
|
||||
* Conventions match the other facade families:
|
||||
* - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
|
||||
* - String output uses the buf/size convention.
|
||||
* - Booleans are int (1/0).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Callback invoked when the engine requests the disk cache settings
|
||||
* dialog for a folder.
|
||||
*
|
||||
* `folder_path` is the UTF-8 path of the cache folder. `parent_window` is a
|
||||
* borrowed pointer to the QWidget that should act as the dialog's parent (may
|
||||
* be NULL). `userdata` is the value passed to
|
||||
* oakengine_disk_set_settings_handler().
|
||||
*/
|
||||
typedef void (*oakengine_disk_settings_fn)(const char *folder_path,
|
||||
void *parent_window,
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Create the DiskManager singleton.
|
||||
*
|
||||
* Safe to call when the instance already exists (no-op). Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_FAILED.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_create_instance(void);
|
||||
|
||||
/**
|
||||
* @brief Destroy the DiskManager singleton.
|
||||
*
|
||||
* Safe to call when no instance exists (no-op). Returns OAKENGINE_OK.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_destroy_instance(void);
|
||||
|
||||
/**
|
||||
* @brief Register the handler used to show the disk cache settings dialog.
|
||||
*
|
||||
* The engine calls this handler when the user requests the settings dialog.
|
||||
* Passing NULL clears the handler. Returns OAKENGINE_OK.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_set_settings_handler(
|
||||
oakengine_disk_settings_fn fn, void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Show the disk cache settings dialog for `path`.
|
||||
*
|
||||
* If `path` is NULL or empty, the default cache folder is used. The actual
|
||||
* dialog is shown by the handler registered with
|
||||
* oakengine_disk_set_settings_handler(); if no handler is registered the
|
||||
* request is logged and skipped. Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_show_settings_dialog(const char *path,
|
||||
void *parent_window);
|
||||
|
||||
/**
|
||||
* @brief Show a confirmation dialog before changing the disk cache location.
|
||||
*
|
||||
* Returns 1 if the user confirms, 0 otherwise. `parent_window` may be NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_show_change_confirmation_dialog(
|
||||
void *parent_window);
|
||||
|
||||
/**
|
||||
* @brief Clear the disk cache in `path`.
|
||||
*
|
||||
* Returns 1 on success, 0 on failure. The folder is opened if necessary.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_clear_cache(const char *path);
|
||||
|
||||
/**
|
||||
* @brief Get the default cache folder path (buf/size convention).
|
||||
*
|
||||
* Returns the string length on success, or a negative OAKENGINE_E_* code when
|
||||
* no DiskManager instance exists.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_get_default_cache_path(char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Set the default cache folder path.
|
||||
*
|
||||
* The default folder's path is updated and will be persisted when the
|
||||
* DiskManager instance is destroyed. Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_set_default_cache_path(const char *path);
|
||||
|
||||
/**
|
||||
* @brief Get or create a borrowed opaque handle to the cache folder for
|
||||
* `path`.
|
||||
*
|
||||
* Returns NULL if no DiskManager instance exists or if `path` is invalid. If
|
||||
* `path` is NULL or empty, the default cache folder is returned. The returned
|
||||
* handle is a borrowed pointer whose lifetime follows the DiskManager
|
||||
* instance; it must not be freed.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_disk_get_open_folder(const char *path);
|
||||
|
||||
/**
|
||||
* @brief Emit the invalidate_project signal on the DiskManager instance.
|
||||
*
|
||||
* This tells consumers of the disk cache that `project` has changed and any
|
||||
* cached data for it should be discarded. Returns OAKENGINE_OK or an error
|
||||
* code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_disk_invalidate_project(
|
||||
OakEngineProject *project);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_DISK_H */
|
||||
@@ -0,0 +1,186 @@
|
||||
/***
|
||||
|
||||
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_DISPLAY_H
|
||||
#define OAKENGINE_DISPLAY_H
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file display.h
|
||||
* @brief C ABI for the GPU display renderer used by viewer/scope widgets
|
||||
*
|
||||
* This family wraps the engine's interactive display renderer
|
||||
* (olive::Renderer and its OpenGLRenderer/DynamicRenderer implementations,
|
||||
* engine/render/renderer.h) plus the GPU texture (olive::Texture) and the
|
||||
* CPU frame buffer (olive::Frame) that viewer/scope widgets use to move
|
||||
* pixels between the CPU and the GPU.
|
||||
*
|
||||
* It is distinct from the sequence-rendering facade in oakengine/renderer.h
|
||||
* (OakEngineRenderer), which pulls finished CPU frames out of the async
|
||||
* render pipeline. This family drives the *on-screen* paint path instead:
|
||||
* a widget creates a renderer, initializes it with the widget's GL context,
|
||||
* uploads/downloads textures, and blits color-managed images each paint.
|
||||
*
|
||||
* Conventions (matching the other facade families):
|
||||
* - All object pointers are opaque. `renderer` is an olive::Renderer*,
|
||||
* `texture` an olive::Texture*, `frame` an olive::Frame*.
|
||||
* - `out_texture` / `out_frame` are pointers to caller-owned
|
||||
* olive::TexturePtr / olive::FramePtr (std::shared_ptr) storage; the
|
||||
* callee assigns a newly created smart pointer into them, releasing any
|
||||
* previously held object. This keeps shared-pointer ownership/deleter
|
||||
* bookkeeping entirely on the engine side.
|
||||
* - `video_params` is a `const olive::VideoParams*`; `color_job` is a
|
||||
* `const olive::ColorTransformJob*`. These are passed as opaque pointers
|
||||
* because they are C++ types; both the caller (app) and the callee
|
||||
* (engine) are compiled as C++ against the same headers.
|
||||
* - `gl_context` is a `QOpenGLContext*` or NULL.
|
||||
* - `parent` is the owning `QObject*` (the display widget); the created
|
||||
* renderer is a QObject child of it and is destroyed by Qt ownership.
|
||||
* Do NOT call oakengine_display_renderer_destroy() and then also rely on
|
||||
* Qt deletion of the same renderer's GPU resources -- destroy() releases
|
||||
* GPU state, Qt deletion releases the object.
|
||||
*/
|
||||
|
||||
/* ---- Display renderer lifecycle ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Create a dynamic-backend renderer (olive::DynamicRenderer) for
|
||||
* `backend_name` and load() it.
|
||||
*
|
||||
* @return The renderer (olive::Renderer*), or NULL if the backend library
|
||||
* could not be loaded (the failed renderer is deleted internally and
|
||||
* the caller should fall back to
|
||||
* oakengine_display_renderer_create_opengl()). NULL is also returned
|
||||
* when the engine was built without dynamic-backend support.
|
||||
*/
|
||||
OAKENGINE_API void *
|
||||
oakengine_display_renderer_create_dynamic(const char *backend_name,
|
||||
void *parent);
|
||||
|
||||
/**
|
||||
* @brief Create the built-in OpenGL renderer (olive::OpenGLRenderer).
|
||||
*
|
||||
* @return The renderer (olive::Renderer*), never NULL.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_display_renderer_create_opengl(void *parent);
|
||||
|
||||
/**
|
||||
* @brief Initialize a display renderer and run its post-init step.
|
||||
*
|
||||
* If `gl_context` is non-NULL the OpenGL/dynamic path is taken (the renderer
|
||||
* is initialized against the widget's shared QOpenGLContext); otherwise the
|
||||
* backend-neutral path (Renderer::init()/post_init()) is used.
|
||||
*
|
||||
* @return OAKENGINE_OK on success, OAKENGINE_E_INVALID for a NULL renderer.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_display_renderer_init(void *renderer,
|
||||
void *gl_context);
|
||||
|
||||
/**
|
||||
* @brief Release a display renderer's GPU resources (Renderer::destroy()
|
||||
* followed by post_destroy()). The renderer object itself remains owned by
|
||||
* its Qt parent.
|
||||
*/
|
||||
OAKENGINE_API void oakengine_display_renderer_destroy(void *renderer);
|
||||
|
||||
/* ---- Texture creation and pixel transfer -------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Create a GPU texture on `renderer` (Renderer::create_texture()).
|
||||
*
|
||||
* @param renderer olive::Renderer*.
|
||||
* @param video_params const olive::VideoParams* describing the texture.
|
||||
* @param pixels Initial pixel data, or NULL for an empty texture.
|
||||
* @param linesize Line stride of `pixels` (ignored when NULL).
|
||||
* @param out_texture Pointer to an olive::TexturePtr to receive the result.
|
||||
*/
|
||||
OAKENGINE_API void
|
||||
oakengine_display_renderer_create_texture(void *renderer,
|
||||
const void *video_params,
|
||||
const void *pixels, int linesize,
|
||||
void *out_texture);
|
||||
|
||||
/**
|
||||
* @brief Blit a color-managed image (Renderer::blit_color_managed()).
|
||||
*
|
||||
* @param renderer olive::Renderer*.
|
||||
* @param color_job const olive::ColorTransformJob*.
|
||||
* @param dst_texture Destination olive::Texture*, or NULL to blit to the
|
||||
* current output destination.
|
||||
* @param video_params const olive::VideoParams* for the destination, or NULL
|
||||
* to use dst_texture's own parameters (in which case
|
||||
* dst_texture must be non-NULL).
|
||||
*/
|
||||
OAKENGINE_API void
|
||||
oakengine_display_renderer_blit_color_managed(void *renderer,
|
||||
const void *color_job,
|
||||
void *dst_texture,
|
||||
const void *video_params);
|
||||
|
||||
/**
|
||||
* @brief Upload CPU pixels into a GPU texture (Texture::upload()).
|
||||
*/
|
||||
OAKENGINE_API void oakengine_display_texture_upload(void *texture,
|
||||
void *pixels, int linesize);
|
||||
|
||||
/**
|
||||
* @brief Download GPU texture pixels into CPU memory (Texture::download()).
|
||||
*/
|
||||
OAKENGINE_API void oakengine_display_texture_download(void *texture,
|
||||
void *pixels,
|
||||
int linesize);
|
||||
|
||||
/* ---- CPU frame buffer --------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Create an empty CPU frame (olive::Frame::create()).
|
||||
*
|
||||
* @param out_frame Pointer to an olive::FramePtr to receive the new frame.
|
||||
*/
|
||||
OAKENGINE_API void oakengine_codec_frame_create(void *out_frame);
|
||||
|
||||
/**
|
||||
* @brief Set a frame's video parameters (Frame::set_video_params()).
|
||||
*
|
||||
* @param frame olive::Frame*.
|
||||
* @param video_params const olive::VideoParams*.
|
||||
*/
|
||||
OAKENGINE_API void oakengine_codec_frame_set_video_params(void *frame,
|
||||
const void
|
||||
*video_params);
|
||||
|
||||
/**
|
||||
* @brief Allocate the frame's pixel buffer (Frame::allocate()).
|
||||
*
|
||||
* @return 1 on success, 0 on failure or NULL frame.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_codec_frame_allocate(void *frame);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_DISPLAY_H */
|
||||
@@ -0,0 +1,505 @@
|
||||
/***
|
||||
|
||||
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_ENCODING_H
|
||||
#define OAKENGINE_ENCODING_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
#include "timeline.h"
|
||||
#include "videoparams.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file encoding.h
|
||||
* @brief C ABI for the encoding parameter surface (EncodingParams /
|
||||
* ExportFormat / ExportCodec)
|
||||
*
|
||||
* This family exposes everything the application's export dialog (and the
|
||||
* audio-recording path) needs without touching the engine's C++ classes:
|
||||
*
|
||||
* - Container/codec metadata queries (format names/extensions, codec lists
|
||||
* per format, codec names/flags, supported pixel and sample formats).
|
||||
* - An opaque OakEngineEncodingParams handle wrapping the engine's
|
||||
* EncodingParams: full getter/setter surface, preset path/listing and
|
||||
* preset load/save.
|
||||
* - oakengine_export_render_with_params(): runs the same synchronous export
|
||||
* path as oakengine_export_render_ex() (oakengine/exporter.h) using a
|
||||
* params handle assembled through this family.
|
||||
*
|
||||
* Enum int fields carry the engine's own enum values
|
||||
* (olive::ExportFormat::Format, olive::ExportCodec::Codec,
|
||||
* olive::VideoParams::Interlacing/ColorRange, olive::PixelFormat::Format,
|
||||
* olive::core::SampleFormat::Format). Conventions match the other facade
|
||||
* families: 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes, buf/size
|
||||
* strings (return value is the would-be length excluding the NUL), NULL
|
||||
* handles are no-ops returning the documented failure value.
|
||||
*/
|
||||
|
||||
/** @brief Opaque encoding-parameters handle (olive::EncodingParams). */
|
||||
typedef struct OakEngineEncodingParams OakEngineEncodingParams;
|
||||
|
||||
/** @brief Scaling method values (EncodingParams::VideoScalingMethod). */
|
||||
#define OAKENGINE_ENCODING_SCALING_FIT 0
|
||||
#define OAKENGINE_ENCODING_SCALING_STRETCH 1
|
||||
#define OAKENGINE_ENCODING_SCALING_CROP 2
|
||||
|
||||
/**
|
||||
* @brief Container formats (olive::ExportFormat::Format) referenced by name
|
||||
* in UI code. Only append; the values are serialized in project/preset
|
||||
* files. The complete list lives in engine/codec/exportformat.h.
|
||||
*/
|
||||
#define OAKENGINE_ENCODING_FORMAT_MATROSKA 1
|
||||
#define OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO 2
|
||||
#define OAKENGINE_ENCODING_FORMAT_QUICKTIME 4
|
||||
#define OAKENGINE_ENCODING_FORMAT_PNG 5
|
||||
#define OAKENGINE_ENCODING_FORMAT_WAV 7
|
||||
#define OAKENGINE_ENCODING_FORMAT_SRT 13
|
||||
|
||||
/**
|
||||
* @brief Codecs (olive::ExportCodec::Codec) referenced by name in UI code.
|
||||
* Only append; the values are serialized. The complete list lives in
|
||||
* engine/codec/exportcodec.h.
|
||||
*/
|
||||
#define OAKENGINE_ENCODING_CODEC_H264 1
|
||||
#define OAKENGINE_ENCODING_CODEC_H264RGB 2
|
||||
#define OAKENGINE_ENCODING_CODEC_H265 3
|
||||
#define OAKENGINE_ENCODING_CODEC_CINEFORM 7
|
||||
#define OAKENGINE_ENCODING_CODEC_AAC 12
|
||||
#define OAKENGINE_ENCODING_CODEC_PCM 13
|
||||
#define OAKENGINE_ENCODING_CODEC_SRT 17
|
||||
#define OAKENGINE_ENCODING_CODEC_AV1 18
|
||||
|
||||
/** @brief olive::VideoParams::ColorRange values. */
|
||||
#define OAKENGINE_ENCODING_COLOR_RANGE_LIMITED 0
|
||||
#define OAKENGINE_ENCODING_COLOR_RANGE_FULL 1
|
||||
|
||||
/** @brief olive::VideoParams::Interlacing values. */
|
||||
#define OAKENGINE_ENCODING_INTERLACE_NONE 0
|
||||
#define OAKENGINE_ENCODING_INTERLACE_TOP_FIRST 1
|
||||
#define OAKENGINE_ENCODING_INTERLACE_BOTTOM_FIRST 2
|
||||
|
||||
/* ---- Container format / codec metadata ---------------------------------- */
|
||||
|
||||
/** @brief Number of container formats (olive::ExportFormat::k_format_count). */
|
||||
OAKENGINE_API int oakengine_encoding_format_count(void);
|
||||
|
||||
/** @brief Display name of a container format (buf/size); -1 invalid. */
|
||||
OAKENGINE_API int oakengine_encoding_format_name(int format, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/** @brief File extension (no dot) of a container format (buf/size). */
|
||||
OAKENGINE_API int oakengine_encoding_format_extension(int format, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Number of video codecs a container format supports; -1 when the
|
||||
* format is invalid.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_format_video_codec_count(int format);
|
||||
|
||||
/**
|
||||
* @brief The `index`-th video codec of `format` as an
|
||||
* olive::ExportCodec::Codec value; -1 when out of range.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_format_video_codec_at(int format,
|
||||
int index);
|
||||
|
||||
/** @brief Audio-codec variant of the two functions above. */
|
||||
OAKENGINE_API int oakengine_encoding_format_audio_codec_count(int format);
|
||||
OAKENGINE_API int oakengine_encoding_format_audio_codec_at(int format,
|
||||
int index);
|
||||
|
||||
/** @brief Subtitle-codec variant of the two functions above. */
|
||||
OAKENGINE_API int oakengine_encoding_format_subtitle_codec_count(int format);
|
||||
OAKENGINE_API int oakengine_encoding_format_subtitle_codec_at(int format,
|
||||
int index);
|
||||
|
||||
/** @brief Display name of a codec (buf/size); -1 when invalid. */
|
||||
OAKENGINE_API int oakengine_encoding_codec_name(int codec, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/** @brief 1 when `codec` encodes still images (PNG/TIFF/OpenEXR). */
|
||||
OAKENGINE_API int oakengine_encoding_codec_is_still_image(int codec);
|
||||
|
||||
/** @brief 1 when `codec` is lossless (no bit-rate setting applies). */
|
||||
OAKENGINE_API int oakengine_encoding_codec_is_lossless(int codec);
|
||||
|
||||
/**
|
||||
* @brief Number of encoded pixel formats (e.g. "yuv420p") usable with
|
||||
* `codec` inside `format`; -1 when invalid.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_pix_fmt_count(int format, int codec);
|
||||
|
||||
/** @brief The `index`-th encoded pixel format name (buf/size). */
|
||||
OAKENGINE_API int oakengine_encoding_pix_fmt_at(int format, int codec,
|
||||
int index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Index of `pix_fmt` (e.g. "yuv420p") in `codec`'s supported pixel
|
||||
* format list; 0 (the codec's preferred format) when absent or `pix_fmt` is
|
||||
* NULL/empty.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_pix_fmt_index(int codec,
|
||||
const char *pix_fmt);
|
||||
|
||||
/**
|
||||
* @brief Number of sample formats usable with `codec` inside `format`;
|
||||
* -1 when invalid.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_sample_format_count(int format,
|
||||
int codec);
|
||||
|
||||
/**
|
||||
* @brief The `index`-th sample format as an olive::core::SampleFormat::Format
|
||||
* value; -1 when out of range.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_sample_format_at(int format, int codec,
|
||||
int index);
|
||||
|
||||
/* ---- Image-sequence filename helpers (olive::Encoder statics) ----------- */
|
||||
|
||||
/** @brief 1 when `filename` contains a "[#####]" digit placeholder. */
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_filename_contains_digit_placeholder(const char *filename);
|
||||
|
||||
/**
|
||||
* @brief Digit count of the filename's "[#####]" placeholder; 0 when none.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_image_sequence_digit_count(const char *filename);
|
||||
|
||||
/** @brief `filename` with the digit placeholder removed (buf/size). */
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_filename_remove_digit_placeholder(const char *filename,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Fit/stretch/crop transform matrix
|
||||
* (EncodingParams::generate_matrix()).
|
||||
*
|
||||
* Writes the 16 floats of the column-major 4x4 matrix to `out16`
|
||||
* (QMatrix4x4 layout). `method` is OAKENGINE_ENCODING_SCALING_*.
|
||||
*
|
||||
* @return OAKENGINE_OK, or OAKENGINE_E_INVALID for bad arguments.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_generate_matrix(int method, int src_width,
|
||||
int src_height,
|
||||
int dest_width,
|
||||
int dest_height,
|
||||
float out16[16]);
|
||||
|
||||
/* ---- Encoding parameters handle ----------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Create an empty encoding-parameters handle (all tracks disabled,
|
||||
* format unset). Destroy with oakengine_encoding_params_destroy().
|
||||
*/
|
||||
OAKENGINE_API OakEngineEncodingParams *oakengine_encoding_params_create(void);
|
||||
|
||||
/** @brief Destroy a handle created by oakengine_encoding_params_create(). */
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_destroy(OakEngineEncodingParams *params);
|
||||
|
||||
/**
|
||||
* @brief 1 when at least one of video/audio/subtitles is enabled
|
||||
* (EncodingParams::is_valid()).
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_is_valid(const OakEngineEncodingParams *params);
|
||||
|
||||
/** @brief Output filename (buf/size convention). */
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_set_filename(OakEngineEncodingParams *params,
|
||||
const char *filename);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_filename(const OakEngineEncodingParams *params,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Container format as olive::ExportFormat::Format; the getter returns
|
||||
* -1 when unset. The setter rejects out-of-range values with
|
||||
* OAKENGINE_E_INVALID.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_set_format(OakEngineEncodingParams *params,
|
||||
int format);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_format(const OakEngineEncodingParams *params);
|
||||
|
||||
/**
|
||||
* @brief Enable video with the given parameters and codec
|
||||
* (EncodingParams::enable_video()).
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_enable_video(OakEngineEncodingParams *params,
|
||||
const oak_video_params *video,
|
||||
int codec);
|
||||
|
||||
/**
|
||||
* @brief Enable audio (EncodingParams::enable_audio()). `sample_format` is
|
||||
* an olive::core::SampleFormat::Format value.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_enable_audio(OakEngineEncodingParams *params,
|
||||
int sample_rate,
|
||||
uint64_t channel_layout,
|
||||
int sample_format, int codec);
|
||||
|
||||
/** @brief Enable embedded subtitles. */
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_enable_subtitles(OakEngineEncodingParams *params,
|
||||
int codec);
|
||||
|
||||
/** @brief Enable sidecar subtitles with the given sidecar container. */
|
||||
OAKENGINE_API int oakengine_encoding_params_enable_sidecar_subtitles(
|
||||
OakEngineEncodingParams *params, int format, int codec);
|
||||
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_disable_video(OakEngineEncodingParams *params);
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_disable_audio(OakEngineEncodingParams *params);
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_disable_subtitles(OakEngineEncodingParams *params);
|
||||
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_video_enabled(const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_video_codec(const OakEngineEncodingParams *params);
|
||||
|
||||
/**
|
||||
* @brief Read back the video parameters (any field may be NULL);
|
||||
* OAKENGINE_E_STATE when video is disabled.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_params_get_video_params(
|
||||
const OakEngineEncodingParams *params, oak_video_params *out);
|
||||
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_audio_enabled(const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_audio_codec(const OakEngineEncodingParams *params);
|
||||
|
||||
/**
|
||||
* @brief Read back the audio parameters (any field may be NULL);
|
||||
* OAKENGINE_E_STATE when audio is disabled.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_params_get_audio_params(
|
||||
const OakEngineEncodingParams *params, int *sample_rate,
|
||||
uint64_t *channel_layout, int *sample_format);
|
||||
|
||||
OAKENGINE_API int oakengine_encoding_params_subtitles_enabled(
|
||||
const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API int oakengine_encoding_params_subtitles_are_sidecar(
|
||||
const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API int oakengine_encoding_params_subtitles_sidecar_format(
|
||||
const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API int oakengine_encoding_params_subtitles_codec(
|
||||
const OakEngineEncodingParams *params);
|
||||
|
||||
/** @brief Video bit rates / buffer size (bit/s, bytes). */
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_video_bit_rate(OakEngineEncodingParams *params,
|
||||
int64_t rate);
|
||||
OAKENGINE_API int64_t
|
||||
oakengine_encoding_params_video_bit_rate(const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_video_min_bit_rate(
|
||||
OakEngineEncodingParams *params, int64_t rate);
|
||||
OAKENGINE_API int64_t oakengine_encoding_params_video_min_bit_rate(
|
||||
const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_video_max_bit_rate(
|
||||
OakEngineEncodingParams *params, int64_t rate);
|
||||
OAKENGINE_API int64_t oakengine_encoding_params_video_max_bit_rate(
|
||||
const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_video_buffer_size(
|
||||
OakEngineEncodingParams *params, int64_t size);
|
||||
OAKENGINE_API int64_t oakengine_encoding_params_video_buffer_size(
|
||||
const OakEngineEncodingParams *params);
|
||||
|
||||
/** @brief Encoder thread count (0 = auto). */
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_video_threads(OakEngineEncodingParams *params,
|
||||
int threads);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_video_threads(const OakEngineEncodingParams *params);
|
||||
|
||||
/** @brief Audio bit rate (bit/s). */
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_audio_bit_rate(OakEngineEncodingParams *params,
|
||||
int64_t rate);
|
||||
OAKENGINE_API int64_t
|
||||
oakengine_encoding_params_audio_bit_rate(const OakEngineEncodingParams *params);
|
||||
|
||||
/** @brief Encoded pixel format name (e.g. "yuv420p"; buf/size getter). */
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_set_video_pix_fmt(OakEngineEncodingParams *params,
|
||||
const char *pix_fmt);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_video_pix_fmt(
|
||||
const OakEngineEncodingParams *params, char *buf, int buf_size);
|
||||
|
||||
/** @brief Image-sequence flag (0/1). */
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_video_is_image_sequence(
|
||||
OakEngineEncodingParams *params, int is_image_sequence);
|
||||
OAKENGINE_API int oakengine_encoding_params_video_is_image_sequence(
|
||||
const OakEngineEncodingParams *params);
|
||||
|
||||
/**
|
||||
* @brief Output color transform by OCIO color space name; an empty/NULL
|
||||
* name selects the reference space (no transform).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_encoding_params_set_color_transform(
|
||||
OakEngineEncodingParams *params, const char *output_name);
|
||||
OAKENGINE_API int oakengine_encoding_params_color_transform_output(
|
||||
const OakEngineEncodingParams *params, char *buf, int buf_size);
|
||||
|
||||
/** @brief Export length as rational seconds. */
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_export_length(OakEngineEncodingParams *params,
|
||||
int num, int den);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_get_export_length(
|
||||
const OakEngineEncodingParams *params, int *num, int *den);
|
||||
|
||||
/**
|
||||
* @brief Custom export range as rational seconds [in, out). The getter
|
||||
* returns OAKENGINE_E_NOT_FOUND when no custom range is set.
|
||||
*/
|
||||
OAKENGINE_API void
|
||||
oakengine_encoding_params_set_custom_range(OakEngineEncodingParams *params,
|
||||
int64_t in_num, int64_t in_den,
|
||||
int64_t out_num, int64_t out_den);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_has_custom_range(
|
||||
const OakEngineEncodingParams *params);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_get_custom_range(
|
||||
const OakEngineEncodingParams *params, int64_t *in_num, int64_t *in_den,
|
||||
int64_t *out_num, int64_t *out_den);
|
||||
|
||||
/** @brief Scaling method (OAKENGINE_ENCODING_SCALING_*). */
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_set_video_scaling_method(
|
||||
OakEngineEncodingParams *params, int method);
|
||||
OAKENGINE_API int oakengine_encoding_params_video_scaling_method(
|
||||
const OakEngineEncodingParams *params);
|
||||
|
||||
/**
|
||||
* @brief Encoder-specific video option (key/value strings, e.g. "crf" =
|
||||
* "18"); mirrors EncodingParams::set_video_option(). The getter returns the
|
||||
* would-be length (buf/size) or OAKENGINE_E_NOT_FOUND when the key is unset.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_set_video_option(OakEngineEncodingParams *params,
|
||||
const char *key, const char *value);
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_video_option(const OakEngineEncodingParams *params,
|
||||
const char *key, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/* ---- Presets ------------------------------------------------------------- */
|
||||
|
||||
/** @brief Directory where export presets live (buf/size). */
|
||||
OAKENGINE_API int oakengine_encoding_preset_path(char *buf, int buf_size);
|
||||
|
||||
/** @brief Number of saved presets. */
|
||||
OAKENGINE_API int oakengine_encoding_preset_count(void);
|
||||
|
||||
/** @brief Name of the `index`-th preset (buf/size); -1 when out of range. */
|
||||
OAKENGINE_API int oakengine_encoding_preset_name(int index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Load parameters from a preset/XML file (overwrites the handle's
|
||||
* contents on success).
|
||||
*
|
||||
* @return OAKENGINE_OK, OAKENGINE_E_INVALID for bad arguments, or
|
||||
* OAKENGINE_E_FAILED when the file cannot be read or parsed.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_load_file(OakEngineEncodingParams *params,
|
||||
const char *path);
|
||||
|
||||
/** @brief Save parameters to a preset/XML file (same return convention). */
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_params_save_file(const OakEngineEncodingParams *params,
|
||||
const char *path);
|
||||
|
||||
/* ---- Export execution / per-sequence last-used --------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Run a synchronous offline export with a params handle assembled
|
||||
* through this family.
|
||||
*
|
||||
* Same blocking/progress/cancel semantics as oakengine_export_render_ex()
|
||||
* (oakengine/exporter.h): progress via
|
||||
* oakengine_export_set_progress_callback(), cancellation via
|
||||
* oakengine_export_cancel(), failure reason via
|
||||
* oakengine_export_last_error(). The output filename and image-sequence
|
||||
* template come from the handle itself.
|
||||
*
|
||||
* @return OAKENGINE_OK / OAKENGINE_E_INVALID / OAKENGINE_E_STATE /
|
||||
* OAKENGINE_E_FAILED / OAKENGINE_E_CANCELLED.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_export_render_with_params(OakEngineSequence *seq,
|
||||
const OakEngineEncodingParams *params);
|
||||
|
||||
/**
|
||||
* @brief Copy of the sequence's last-used encoding parameters
|
||||
* (ViewerOutput::get_last_used_encoding_params()), or NULL when none is
|
||||
* valid. Caller destroys with oakengine_encoding_params_destroy().
|
||||
*/
|
||||
OAKENGINE_API OakEngineEncodingParams *
|
||||
oakengine_encoding_params_get_last_used(OakEngineSequence *seq);
|
||||
|
||||
/**
|
||||
* @brief Store `params` as the sequence's last-used encoding parameters
|
||||
* (ViewerOutput::set_last_used_encoding_params()); NULL is a no-op.
|
||||
*/
|
||||
OAKENGINE_API void oakengine_encoding_params_set_last_used(
|
||||
OakEngineSequence *seq, const OakEngineEncodingParams *params);
|
||||
|
||||
/**
|
||||
* @brief Start audio recording to the file described by `params`
|
||||
* (AudioManager::start_recording(); audio must be enabled on the handle).
|
||||
*
|
||||
* @return OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad arguments;
|
||||
* OAKENGINE_E_STATE when the audio manager is not running;
|
||||
* OAKENGINE_E_FAILED otherwise (a human-readable reason is written to
|
||||
* `errbuf`/`errbuf_size` when given).
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_encoding_start_audio_recording(const OakEngineEncodingParams *params,
|
||||
char *errbuf, int errbuf_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_ENCODING_H */
|
||||
@@ -0,0 +1,263 @@
|
||||
/***
|
||||
|
||||
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_EVENTS_H
|
||||
#define OAKENGINE_EVENTS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "project.h"
|
||||
#include "timeline.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file events.h
|
||||
* @brief C ABI for engine change notifications (the signal/slot replacement)
|
||||
*
|
||||
* The engine's C++ API notifies observers through Qt signals (Project::
|
||||
* modified_changed, Folder::begin_insert_item, Track::block_added, the
|
||||
* sequence's track/marker/workarea notifications, ...). This family exposes
|
||||
* the same notifications to C consumers as a subscription/callback
|
||||
* mechanism, so the application never needs to connect() to an engine
|
||||
* QObject directly.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* int64_t sub = oakengine_event_subscribe(handle, OAKENGINE_EVENT_..., fn,
|
||||
* userdata);
|
||||
* ...
|
||||
* oakengine_event_unsubscribe(sub);
|
||||
*
|
||||
* `handle` is a borrowed facade handle whose static type depends on the
|
||||
* event family (see the table below); a mismatch or NULL handle fails with
|
||||
* 0 (an invalid subscription id). Subscribing the same (handle, event)
|
||||
* twice is allowed and returns two independent subscription ids.
|
||||
*
|
||||
* Thread semantics: callbacks are invoked SYNCHRONOUSLY on the thread that
|
||||
* emits the change (the equivalent of Qt::DirectConnection) before the
|
||||
* engine's own emission returns, exactly like the C++ connections they
|
||||
* replace. The callback runs under whatever locks the engine holds at the
|
||||
* emission site; it must not call back into editing primitives that mutate
|
||||
* the same object. All engine objects live on the GUI thread, so callbacks
|
||||
* normally fire there.
|
||||
*
|
||||
* Lifetime: the registry drops the subscription automatically when the
|
||||
* observed engine object is destroyed, so a stale subscription id is never
|
||||
* a use-after-free; oakengine_event_unsubscribe() on an id whose object
|
||||
* died is a harmless no-op returning OAKENGINE_E_NOT_FOUND. The inverse is
|
||||
* NOT tracked: `userdata` ownership stays with the subscriber, which must
|
||||
* unsubscribe (or tolerate callbacks) until its own teardown.
|
||||
*
|
||||
* Event payloads use POD fields only. Timestamps are frame numbers in the
|
||||
* owning sequence's frame-rate timebase (same convention as timeline.h);
|
||||
* `handle`/`source` are borrowed pointers the callee may use during the
|
||||
* callback only.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Event ids for oakengine_event_subscribe().
|
||||
*
|
||||
* handle column: the facade handle to pass for that event.
|
||||
* payload column: oakengine_event field contents on delivery.
|
||||
*/
|
||||
#define OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED 1 /**< handle: OakEngineProject*. a = new modified flag (0/1). */
|
||||
#define OAKENGINE_EVENT_PROJECT_NAME_CHANGED 2 /**< handle: OakEngineProject*. no payload. */
|
||||
|
||||
#define OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM 10 /**< handle: OakEngineNode* (a folder). handle field = child OakEngineNode*, a = insertion index. */
|
||||
#define OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM 11 /**< handle: OakEngineNode* (a folder). no payload. */
|
||||
#define OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM 12 /**< handle: OakEngineNode* (a folder). handle field = child OakEngineNode*, a = child index. */
|
||||
#define OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM 13 /**< handle: OakEngineNode* (a folder). no payload. */
|
||||
|
||||
#define OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED 20 /**< handle: OakEngineSequence*. handle field = OakEngineTrack*, a = track type (OAKENGINE_TRACK_TYPE_*). */
|
||||
#define OAKENGINE_EVENT_SEQUENCE_TRACK_REMOVED 21 /**< handle: OakEngineSequence*. handle field = OakEngineTrack*, a = track type. */
|
||||
#define OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED 22 /**< handle: OakEngineSequence*. a = track type. Fired on TrackList::track_list_changed (order/label-affecting changes). */
|
||||
#define OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED 23 /**< handle: OakEngineSequence*. handle field = OakEngineTrack*, a = track type, b = new height in PIXELS (TrackList::track_height_changed). */
|
||||
#define OAKENGINE_EVENT_SEQUENCE_SUBTITLES_CHANGED 24 /**< handle: OakEngineSequence*. a/b = changed range in/out (ts). */
|
||||
|
||||
#define OAKENGINE_EVENT_TRACK_BLOCK_ADDED 30 /**< handle: OakEngineTrack*. handle field = OakEngineBlock*, a = block in-point (ts), b = block out-point (ts). */
|
||||
#define OAKENGINE_EVENT_TRACK_BLOCK_REMOVED 31 /**< handle: OakEngineTrack*. handle field = OakEngineBlock*, a = in (ts), b = out (ts) at removal time. */
|
||||
#define OAKENGINE_EVENT_TRACK_INDEX_CHANGED 32 /**< handle: OakEngineTrack*. a = old index, b = new index. */
|
||||
#define OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED 33 /**< handle: OakEngineTrack*. a = int64 bit-cast of the new height (double, internal units; memcpy to decode). */
|
||||
#define OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED 34 /**< handle: OakEngineTrack*. no payload (Track::blocks_refreshed). */
|
||||
#define OAKENGINE_EVENT_TRACK_MUTED_CHANGED 35 /**< handle: OakEngineTrack*. a = muted 0/1. */
|
||||
|
||||
#define OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED 36 /**< handle: OakEngineBlock*. no payload (Block::enabled_changed; re-read via oakengine_block_is_enabled). */
|
||||
#define OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED 37 /**< handle: OakEngineBlock*. no payload (Block::preview_changed). */
|
||||
|
||||
#define OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED 40 /**< handle: OakEngineSequence*. a = marker in-point (ts). */
|
||||
#define OAKENGINE_EVENT_SEQUENCE_MARKER_REMOVED 41 /**< handle: OakEngineSequence*. a = marker in-point (ts). */
|
||||
#define OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED 42 /**< handle: OakEngineSequence*. a = marker in-point (ts). */
|
||||
|
||||
#define OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED 50 /**< handle: OakEngineSequence*. a = in (ts), b = out (ts). */
|
||||
#define OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED 51 /**< handle: OakEngineSequence*. a = enabled flag (0/1). */
|
||||
|
||||
#define OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED 60 /**< handle: OakEngineColorManager*. no payload. Fired when the OCIO config changes (ColorManager::config_changed). */
|
||||
#define OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED 61 /**< handle: OakEngineColorManager*. no payload (ColorManager::reference_space_changed). */
|
||||
|
||||
/* Node family (handle: OakEngineNode*). `s` carries the input id where
|
||||
* noted; frame timestamps use the same timebase as oakengine_node_frame_
|
||||
* time_base() (the project's first sequence's frame rate). */
|
||||
#define OAKENGINE_EVENT_NODE_LABEL_CHANGED 70 /**< s = new label. */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED 71 /**< s = input id, a = element, b = range in (ts), c = range out (ts). */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_CONNECTED 72 /**< handle = connected output OakEngineNode*, s = input id, a = element. */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED 73 /**< handle = former output OakEngineNode*, s = input id, a = element. */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_FLAGS_CHANGED 74 /**< s = input id, a = new flags (OAKENGINE_NODE_INPUT_FLAG_*). */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED 75 /**< s = input id (property key/value intentionally omitted; re-read through the node family getters). */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED 76 /**< s = input id, a = new oak_node_value_type. */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED 77 /**< s = input id, a = old size, b = new size. */
|
||||
#define OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED 78 /**< s = input id, a = element, b = enabled (0/1). */
|
||||
#define OAKENGINE_EVENT_NODE_KEYFRAME_ADDED 79 /**< handle = OakEngineKeyframe*, s = input id, a = element, b = track. */
|
||||
#define OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED 80 /**< handle = OakEngineKeyframe* (about to die; use the s/a/b fields, do not dereference), s = input id, a = element, b = track. */
|
||||
#define OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED 81 /**< handle = OakEngineKeyframe*. */
|
||||
#define OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED 82 /**< handle = OakEngineKeyframe*. */
|
||||
#define OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED 83 /**< handle = OakEngineKeyframe*. */
|
||||
#define OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT 84 /**< handle = OakEngineNode* added to this context. */
|
||||
#define OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT 85 /**< handle = OakEngineNode* removed from this context. */
|
||||
#define OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED 86 /**< no payload. */
|
||||
|
||||
/* Group family (handle: OakEngineNode*, must be a group). For 87/88 the
|
||||
* handle field carries the passthrough's inner node, `s` its input id and
|
||||
* `a` its element. */
|
||||
#define OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED 87 /**< handle = inner OakEngineNode*, s = input id, a = element. */
|
||||
#define OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED 88 /**< handle = inner OakEngineNode*, s = input id, a = element. */
|
||||
#define OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED 89 /**< handle = new output OakEngineNode*. */
|
||||
|
||||
/**
|
||||
* handle = OakEngineNode* whose position in this context changed; `a`/`b`
|
||||
* carry the new x/y scene coordinates as int64 bit-casts of double (use
|
||||
* memcpy to decode). */
|
||||
#define OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED 90
|
||||
|
||||
#define OAKENGINE_EVENT_NODE_LINKS_CHANGED 91 /**< no payload (Node::links_changed). */
|
||||
#define OAKENGINE_EVENT_NODE_COLOR_CHANGED 92 /**< no payload (Node::color_changed). */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_ADDED 93 /**< s = input id (Node::input_added). */
|
||||
#define OAKENGINE_EVENT_NODE_INPUT_REMOVED 94 /**< s = input id (Node::input_removed). */
|
||||
#define OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH 95 /**< handle = project OakEngineNode* (Node::removed_from_graph). */
|
||||
|
||||
/* Viewer family (handle: OakEngineNode*, must be a viewer -- validate with
|
||||
* oakengine_viewer_from_node()). Rational payloads (seconds) are carried
|
||||
* as a = numerator, b = denominator. */
|
||||
#define OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED 100 /**< a/b = new length. */
|
||||
#define OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED 101 /**< a/b = new playhead. */
|
||||
#define OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED 102 /**< a/b = new frame rate (NOT flipped). */
|
||||
#define OAKENGINE_EVENT_VIEWER_SIZE_CHANGED 103 /**< a = width, b = height. */
|
||||
#define OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED 104 /**< a/b = new pixel aspect. */
|
||||
#define OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED 105 /**< a = olive::VideoParams::Interlacing. */
|
||||
#define OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED 106 /**< no payload. */
|
||||
#define OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED 107 /**< no payload. */
|
||||
#define OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED 108 /**< no payload. */
|
||||
#define OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED 109 /**< a = new sample rate. */
|
||||
#define OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED 110 /**< no payload. */
|
||||
|
||||
/* Marker list family (handle: OakEngineMarkerList*, from
|
||||
* oakengine_viewer_get_marker_list()). handle field = the OakEngineMarker*
|
||||
* (for REMOVED it is about to die; do not dereference). */
|
||||
#define OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED 111
|
||||
#define OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED 112
|
||||
#define OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED 113
|
||||
|
||||
/* Workarea family (handle: OakEngineWorkarea*, borrowed from
|
||||
* oakengine_viewer_get_workarea_handle() or owned from
|
||||
* oakengine_workarea_create()). */
|
||||
#define OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED 114 /**< no payload; re-read via oakengine_workarea_get(). */
|
||||
#define OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED 115 /**< a = enabled 0/1. */
|
||||
|
||||
/* Task manager family (handle: oakengine_task_manager_handle(), see
|
||||
* oakengine/task.h). The handle field carries the OakEngineTask* (for
|
||||
* REMOVED it is about to die; do not dereference). */
|
||||
#define OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED 120 /**< handle = OakEngineTask*, s = task title. */
|
||||
#define OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED 121 /**< handle = OakEngineTask* (about to die; do not dereference). */
|
||||
#define OAKENGINE_EVENT_TASK_MANAGER_TASK_FAILED 122 /**< handle = OakEngineTask*. */
|
||||
#define OAKENGINE_EVENT_TASK_MANAGER_LIST_CHANGED 123 /**< no payload. */
|
||||
|
||||
/* Task family (handle: OakEngineTask*, see oakengine/task.h). Delivered
|
||||
* synchronously on the thread the task runs on. */
|
||||
#define OAKENGINE_EVENT_TASK_STARTED 125 /**< a = start time (msecs since epoch). */
|
||||
#define OAKENGINE_EVENT_TASK_PROGRESS 126 /**< a = int64 bit-cast of the progress double 0..1 (memcpy to decode). */
|
||||
#define OAKENGINE_EVENT_TASK_FINISHED 127 /**< a = succeeded 0/1. */
|
||||
|
||||
/* Undo stack family (handle: oakengine_undo_handle(), see
|
||||
* oakengine/undo.h). Fires after every stack mutation (push/undo/redo/
|
||||
* jump/clear); re-read the command list through the oakengine_undo_*
|
||||
* accessors. */
|
||||
#define OAKENGINE_EVENT_UNDO_INDEX_CHANGED 130 /**< a = new index (done-command count). */
|
||||
|
||||
/* AudioManager family (handle: oakengine_audio_manager_handle(), see
|
||||
* oakengine/audio.h). Fired when the output device or format changes. */
|
||||
#define OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED 140 /**< no payload. */
|
||||
#define OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY 141 /**< no payload; emitted after each notify interval of audio has been consumed. */
|
||||
|
||||
/* ---- Playback cache / frame cache (B9c) ----------------------------------- */
|
||||
#define OAKENGINE_EVENT_PLAYBACK_CACHE_INVALIDATED 141
|
||||
#define OAKENGINE_EVENT_PLAYBACK_CACHE_VALIDATED 142
|
||||
#define OAKENGINE_EVENT_FRAME_CACHE_INVALIDATED 143
|
||||
|
||||
/**
|
||||
* @brief POD event payload delivered to oakengine_event_fn.
|
||||
*/
|
||||
typedef struct oakengine_event {
|
||||
int32_t id; /**< Event id (OAKENGINE_EVENT_*). */
|
||||
int32_t reserved; /**< Alignment padding; 0. */
|
||||
int64_t a; /**< Event-specific integer payload (see the event table). */
|
||||
int64_t b; /**< Event-specific second integer payload. */
|
||||
int64_t c; /**< Event-specific third integer payload. */
|
||||
void *source; /**< The subscribed handle the event was delivered for (borrowed). */
|
||||
void *handle; /**< Related object, event-specific (borrowed; NULL when none). */
|
||||
const char *s; /**< Event-specific string payload (valid only during the callback; NULL when none). */
|
||||
} oakengine_event;
|
||||
|
||||
/**
|
||||
* @brief Change-notification callback. Invoked synchronously on the
|
||||
* emitting thread; `event` is valid only for the duration of the call.
|
||||
*/
|
||||
typedef void (*oakengine_event_fn)(const oakengine_event *event,
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Subscribe to `event_id` on `handle` (an OakEngineProject*,
|
||||
* OakEngineSequence*, OakEngineTrack* or OakEngineNode* per the event
|
||||
* table) and return a subscription id (> 0).
|
||||
*
|
||||
* Returns 0 on failure: NULL handle/callback, unknown event id, or a
|
||||
* handle whose engine object does not match the event's family. The
|
||||
* callback starts firing with the next matching change; there is no
|
||||
* replay of past state.
|
||||
*/
|
||||
OAKENGINE_API int64_t oakengine_event_subscribe(void *handle, int32_t event_id,
|
||||
oakengine_event_fn fn,
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Cancel a subscription. OAKENGINE_OK on success,
|
||||
* OAKENGINE_E_INVALID for `id` <= 0, OAKENGINE_E_NOT_FOUND for an id that
|
||||
* was never registered or whose engine object has since been destroyed.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_event_unsubscribe(int64_t id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_EVENTS_H */
|
||||
@@ -117,6 +117,24 @@ typedef struct oak_footage_audio_info {
|
||||
int time_base_den; /**< Seconds per time-base unit (denominator). */
|
||||
} oak_footage_audio_info;
|
||||
|
||||
/**
|
||||
* @brief POD proxy generation parameters (olive::ProxyManager::ProxyParams).
|
||||
*
|
||||
* divider: source resolution divider (1 = use absolute width/height,
|
||||
* 2/4/8 = fraction of the source resolution). extension/preset are the
|
||||
* ffmpeg output container and encoder preset (e.g. "mp4"/"veryfast").
|
||||
*/
|
||||
typedef struct oak_proxy_params {
|
||||
int width;
|
||||
int height;
|
||||
int divider;
|
||||
int version;
|
||||
int crf;
|
||||
int include_audio; /**< 1/0. */
|
||||
char extension[32];
|
||||
char preset[32];
|
||||
} oak_proxy_params;
|
||||
|
||||
/**
|
||||
* @brief Probe a media file (decoder, streams, durations, color tags).
|
||||
*
|
||||
@@ -417,6 +435,69 @@ oakengine_footage_colorspace_count(const OakEngineFootage *self);
|
||||
OAKENGINE_API int oakengine_footage_colorspace_at(
|
||||
const OakEngineFootage *self, int index, char *buf, int buf_size);
|
||||
|
||||
/* ---- Footage extras ------------------------------------------------------- */
|
||||
|
||||
/** @brief Filename of the imported footage (buf/size). Returns
|
||||
* OAKENGINE_E_INVALID on NULL. */
|
||||
OAKENGINE_API int oakengine_footage_get_filename(const OakEngineFootage *self,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/** @brief Get the (track_type, stream_index) for the real stream at
|
||||
* `stream_index_in_footage` (which iterates all streams regardless of type).
|
||||
* Returns OAKENGINE_OK or OAKENGINE_E_NOT_FOUND. */
|
||||
OAKENGINE_API int oakengine_footage_get_stream_reference(
|
||||
const OakEngineFootage *self, int stream_index_in_footage,
|
||||
int *out_track_type, int *out_stream_index);
|
||||
|
||||
/** @brief Human-readable description of a video stream (buf/size).
|
||||
* Returns OAKENGINE_E_NOT_FOUND for an out-of-range index. */
|
||||
OAKENGINE_API int oakengine_footage_describe_video_stream(
|
||||
const OakEngineFootage *self, int video_stream_index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/** @brief Human-readable description of an audio stream (buf/size).
|
||||
* Returns OAKENGINE_E_NOT_FOUND for an out-of-range index. */
|
||||
OAKENGINE_API int oakengine_footage_describe_audio_stream(
|
||||
const OakEngineFootage *self, int audio_stream_index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/** @brief Human-readable name of a stream type
|
||||
* (OAKENGINE_TRACK_TYPE_* -> translated name). buf/size convention. */
|
||||
OAKENGINE_API int oakengine_footage_stream_type_name(int track_type, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/** @brief 1 if the footage has custom proxy parameters, 0 otherwise. */
|
||||
OAKENGINE_API int oakengine_footage_has_custom_proxy_params(
|
||||
const OakEngineFootage *self);
|
||||
|
||||
/** @brief Fill `out` with the effective proxy parameters
|
||||
* (custom if set, otherwise the application defaults). */
|
||||
OAKENGINE_API int oakengine_footage_get_effective_proxy_params(
|
||||
const OakEngineFootage *self, oak_proxy_params *out);
|
||||
|
||||
/** @brief Set custom proxy parameters (not undoable). */
|
||||
OAKENGINE_API int oakengine_footage_set_custom_proxy_params(
|
||||
OakEngineFootage *self, const oak_proxy_params *params);
|
||||
|
||||
/** @brief Clear custom proxy parameters, reverting to defaults. */
|
||||
OAKENGINE_API int oakengine_footage_clear_custom_proxy_params(
|
||||
OakEngineFootage *self);
|
||||
|
||||
/** @brief Generate a proxy with the given parameters (synchronous).
|
||||
* `path` is the proxy file path, `state` the proxy state (0=missing,
|
||||
* 1=generating, 2=ready, 3=failed), `stream_index` the video stream index,
|
||||
* `enabled` 1/0 to enable proxy, `version` the preset version. */
|
||||
OAKENGINE_API int oakengine_footage_set_proxy(OakEngineFootage *self,
|
||||
const char *path, int state,
|
||||
int stream_index, int enabled,
|
||||
int version);
|
||||
|
||||
/** @brief Delete the proxy file and reset state. */
|
||||
OAKENGINE_API int oakengine_footage_clear_proxy(OakEngineFootage *self);
|
||||
|
||||
/** @brief Invalidate the footage (force re-probe on next use). */
|
||||
OAKENGINE_API int oakengine_footage_invalidate(OakEngineFootage *self);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAKENGINE_GIZMO_H
|
||||
#define OAKENGINE_GIZMO_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
#include "node.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file gizmo.h
|
||||
* @brief C ABI for gizmo data exchange (text gizmo POD + draggable helpers)
|
||||
*
|
||||
* TextGizmo has been POD-ified: the app retrieves a flat snapshot of the
|
||||
* text v3 node's gizmo state through a single C call instead of holding a
|
||||
* C++ TextGizmo pointer. The 4 Qt signals (activated/deactivated/
|
||||
* rect_changed/vertical_alignment_changed) are replaced by the existing
|
||||
* OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED on the text v3 node.
|
||||
*
|
||||
* DraggableGizmo's drag lifecycle (start/move/end) is exposed as thin C
|
||||
* wrappers so the app can drive dragging without importing engine C++ symbols.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Flat snapshot of a text v3 node's gizmo state.
|
||||
*
|
||||
* Retrieved via oakengine_text_gizmo_get(). The HTML content is accessed
|
||||
* separately through oakengine_text_gizmo_get_html() because it is a
|
||||
* variable-length string.
|
||||
*/
|
||||
typedef struct oakengine_text_gizmo {
|
||||
double rect_x; /**< Bounding rect left */
|
||||
double rect_y; /**< Bounding rect top */
|
||||
double rect_w; /**< Bounding rect width */
|
||||
double rect_h; /**< Bounding rect height */
|
||||
int vertical_alignment; /**< 0 = AlignTop, 1 = AlignBottom, 2 = AlignVCenter */
|
||||
} oakengine_text_gizmo;
|
||||
|
||||
/**
|
||||
* @brief Retrieve the text gizmo POD from a TextGeneratorV3 node.
|
||||
*
|
||||
* @param node The text v3 node (OakEngineNode*). Must be a TextGeneratorV3
|
||||
* (checked at runtime; returns OAKENGINE_E_INVALID otherwise).
|
||||
* @param time_num Numerator of the rational time at which to evaluate.
|
||||
* @param time_den Denominator of the rational time.
|
||||
* @param out Output struct filled on success.
|
||||
* @return OAKENGINE_OK on success, OAKENGINE_E_INVALID if node is not a
|
||||
* TextGeneratorV3 or out is NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_text_gizmo_get(OakEngineNode *node,
|
||||
int64_t time_num, int64_t time_den, oakengine_text_gizmo *out);
|
||||
|
||||
/**
|
||||
* @brief Retrieve the text gizmo's HTML content as a string.
|
||||
*
|
||||
* buf/size convention: pass NULL/0 to get the required length (including
|
||||
* NUL terminator). On success returns the number of bytes written (excluding
|
||||
* NUL). Requires a valid TextGeneratorV3 node.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_text_gizmo_get_html(OakEngineNode *node,
|
||||
int64_t time_num, int64_t time_den, char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Update the HTML content of a text v3 node's text input (undoable).
|
||||
*
|
||||
* Equivalent to the old TextGizmo::update_input_html().
|
||||
*/
|
||||
OAKENGINE_API int oakengine_text_gizmo_update_html(OakEngineNode *node,
|
||||
const char *html, int64_t time_num, int64_t time_den);
|
||||
|
||||
/**
|
||||
* @brief Set the vertical alignment of a text v3 node (undoable).
|
||||
*
|
||||
* `alignment`: 0 = AlignTop, 1 = AlignBottom, 2 = AlignVCenter.
|
||||
* Equivalent to the old TextGizmo::set_vertical_alignment().
|
||||
*/
|
||||
OAKENGINE_API int oakengine_text_gizmo_set_vertical_alignment(
|
||||
OakEngineNode *node, int alignment);
|
||||
|
||||
/**
|
||||
* @brief Notify that a text gizmo has been activated (emits the equivalent of
|
||||
* the old TextGizmo::activated signal via event mechanism).
|
||||
*
|
||||
* Currently a no-op since activation events are app-internal; kept for
|
||||
* API completeness.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_text_gizmo_activated(OakEngineNode *node);
|
||||
|
||||
/**
|
||||
* @brief Notify that a text gizmo has been deactivated.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_text_gizmo_deactivated(OakEngineNode *node);
|
||||
|
||||
/**
|
||||
* @brief Activate/Deactivate the text gizmo on a text v3 node.
|
||||
*
|
||||
* These replace the old TextGizmo::activated()/deactivated() signal emissions.
|
||||
* The app calls these to notify the engine that the text editor opened/closed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Get the DragValueBehavior of a gizmo node.
|
||||
*
|
||||
* Returns: 0 = k_absolute, 1 = k_delta_from_previous, 2 = k_delta_from_start.
|
||||
* Returns OAKENGINE_E_INVALID if the gizmo is not a DraggableGizmo.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_gizmo_get_drag_value_behavior(void *gizmo);
|
||||
|
||||
/**
|
||||
* @brief Start a drag on a DraggableGizmo.
|
||||
*
|
||||
* Wraps DraggableGizmo::drag_start(). The gizmo's internal NodeInputDraggers
|
||||
* are started at the given time. `row` is a pointer to a NodeValueRow
|
||||
* (populated e.g. by oakengine_traverse_generate_row); pass NULL for an
|
||||
* empty row.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_gizmo_drag_start(void *gizmo,
|
||||
void *row, double abs_x, double abs_y, int64_t time_num,
|
||||
int64_t time_den);
|
||||
|
||||
/**
|
||||
* @brief Move a drag (emits handle_movement signal on the gizmo).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_gizmo_drag_move(void *gizmo,
|
||||
double x, double y, int qt_keyboard_modifiers);
|
||||
|
||||
/**
|
||||
* @brief End a drag and push an undoable command.
|
||||
*
|
||||
* @param gizmo The DraggableGizmo pointer (void* for C ABI).
|
||||
* @param command A MultiUndoCommand* (void*) to append undo entries to.
|
||||
* Pass NULL to create a standalone command.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_gizmo_drag_end(void *gizmo, void *command);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // OAKENGINE_GIZMO_H
|
||||
@@ -0,0 +1,87 @@
|
||||
/***
|
||||
|
||||
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_LUT_H
|
||||
#define OAKENGINE_LUT_H
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file lut.h
|
||||
* @brief C ABI for the global LUT file library (olive::LUTLibrary)
|
||||
*
|
||||
* A thin facade over the user-configurable list of LUT directories and the
|
||||
* supported LUT files discovered under them. The library state is kept in the
|
||||
* application config ("LUTLibraryPaths"); this facade only exposes the
|
||||
* directory/file list queries and the directory replacement primitive.
|
||||
*
|
||||
* Conventions match the other facade families:
|
||||
* - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
|
||||
* - String output uses the buf/size convention.
|
||||
* - Count queries return a non-negative integer, or a negative error code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Number of directories currently in the LUT library.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_lut_directory_count(void);
|
||||
|
||||
/**
|
||||
* @brief Get the directory path at `index` (buf/size convention).
|
||||
*
|
||||
* Returns the string length on success, or a negative OAKENGINE_E_* code when
|
||||
* `index` is out of range.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_lut_directory_at(int index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Number of supported LUT files found under the library directories.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_lut_file_count(void);
|
||||
|
||||
/**
|
||||
* @brief Get the full path of the LUT file at `index` (buf/size convention).
|
||||
*
|
||||
* Files are listed in the order they are discovered; files in earlier
|
||||
* directories come first. Returns the string length on success, or a negative
|
||||
* OAKENGINE_E_* code when `index` is out of range.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_lut_file_at(int index, char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Replace the LUT library directories and persist them to config.
|
||||
*
|
||||
* `dirs` is an array of `count` NUL-terminated UTF-8 directory paths. Passing
|
||||
* `count == 0` clears the library. Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_lut_set_directories(const char *const *dirs,
|
||||
int count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_LUT_H */
|
||||
+1079
-1
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
/***
|
||||
|
||||
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_PLUGIN_H
|
||||
#define OAKENGINE_PLUGIN_H
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "node.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file plugin.h
|
||||
* @brief C ABI for plugin support (active viewer, progress reporter, loading)
|
||||
*/
|
||||
|
||||
/* ---- Active viewer provider -------------------------------------------- */
|
||||
|
||||
/** @brief Returns the currently active viewer node (or NULL). */
|
||||
typedef OakEngineNode *(*oakengine_plugin_active_viewer_fn)(void *userdata);
|
||||
|
||||
OAKENGINE_API int oakengine_plugin_set_active_viewer_provider(
|
||||
oakengine_plugin_active_viewer_fn fn, void *userdata);
|
||||
|
||||
/* ---- Progress reporter factory ----------------------------------------- */
|
||||
|
||||
typedef void *(*oakengine_plugin_reporter_create_fn)(
|
||||
const char *message, const char *title, void *userdata);
|
||||
typedef void (*oakengine_plugin_reporter_destroy_fn)(
|
||||
void *reporter, void *userdata);
|
||||
typedef int (*oakengine_plugin_reporter_is_cancelled_fn)(
|
||||
void *reporter, void *userdata);
|
||||
typedef void (*oakengine_plugin_reporter_set_progress_fn)(
|
||||
void *reporter, double progress, void *userdata);
|
||||
|
||||
OAKENGINE_API int oakengine_plugin_set_progress_reporter_factory(
|
||||
oakengine_plugin_reporter_create_fn create,
|
||||
oakengine_plugin_reporter_destroy_fn destroy,
|
||||
oakengine_plugin_reporter_is_cancelled_fn is_cancelled,
|
||||
oakengine_plugin_reporter_set_progress_fn set_progress,
|
||||
void *userdata);
|
||||
|
||||
/* ---- Plugin loading and interaction ------------------------------------ */
|
||||
|
||||
OAKENGINE_API int oakengine_plugin_load_plugins(const char *path);
|
||||
|
||||
OAKENGINE_API int oakengine_plugin_node_push_button_clicked(
|
||||
OakEngineNode *node, const char *button_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_PLUGIN_H */
|
||||
@@ -62,6 +62,24 @@ extern "C" {
|
||||
#define OAKENGINE_LOOP_MODE_LOOP 1 /**< Repeat the clip (olive k_loop_mode_loop). */
|
||||
#define OAKENGINE_LOOP_MODE_CLAMP 2 /**< Hold first/last frame (olive k_loop_mode_clamp). */
|
||||
|
||||
/**
|
||||
* @brief Opaque preview request handle (an active render ticket for
|
||||
* single-frame or audio-range preview).
|
||||
*/
|
||||
typedef struct OakEnginePreviewRequest OakEnginePreviewRequest;
|
||||
|
||||
/**
|
||||
* @brief POD for a single video frame from a preview request
|
||||
* (borrowed data, valid until the request is freed).
|
||||
*/
|
||||
typedef struct oak_playback_frame {
|
||||
int width;
|
||||
int height;
|
||||
int format; /**< olive::PixelFormat::Format value. */
|
||||
const void *data; /**< Planar data pointer (first plane). */
|
||||
int linesize; /**< Bytes per row of the first plane. */
|
||||
} oak_playback_frame;
|
||||
|
||||
/**
|
||||
* @brief Human-readable reason for the last failed preview call on this
|
||||
* thread (buf/size convention).
|
||||
@@ -118,6 +136,108 @@ OAKENGINE_API int oakengine_preview_get_waveform_summary(
|
||||
OakEngineFootage *footage, int channel, int64_t start_ts,
|
||||
int64_t end_ts, double *min_vals, double *max_vals, int count);
|
||||
|
||||
/* ---- R4: waveform, audio levels, cacher, preview requests ------------------ */
|
||||
|
||||
/** @brief Maximum sample rate for waveform generation. > 0. */
|
||||
OAKENGINE_API int oakengine_waveform_max_sample_rate(void);
|
||||
|
||||
/**
|
||||
* @brief Analyze audio levels (linear RMS) from raw float sample data.
|
||||
* `data` is an array of `channels` float pointers, each with `count` samples.
|
||||
* Writes RMS values into `levels` (one per channel). Returns OAKENGINE_OK
|
||||
* or OAKENGINE_E_INVALID on NULL/bad arguments.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_audio_analyze_levels(const float *const *data,
|
||||
int channels, int64_t count,
|
||||
double *levels);
|
||||
|
||||
/**
|
||||
* @brief Set the preview cacher's playhead position (num/den seconds).
|
||||
* Returns OAKENGINE_E_STATE when the cacher is not available.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_preview_cacher_set_playhead(int64_t num,
|
||||
int64_t den);
|
||||
|
||||
/**
|
||||
* @brief Pause or resume thumbnail generation in the cacher.
|
||||
* Returns OAKENGINE_E_STATE when the cacher is not available.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_preview_cacher_set_thumbnails_paused(int paused);
|
||||
|
||||
/**
|
||||
* @brief Clear pending single-frame render requests from the cacher.
|
||||
* Returns OAKENGINE_E_STATE when the cacher is not available.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_preview_cacher_clear_single_frame_renders(int only_finished);
|
||||
|
||||
/**
|
||||
* @brief Force the cacher to cache a range (num/den seconds in/out).
|
||||
* Returns OAKENGINE_E_INVALID on NULL node.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_preview_cacher_force_cache_range(
|
||||
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
|
||||
int64_t out_den);
|
||||
|
||||
/**
|
||||
* @brief Request a single video frame at (num/den) seconds from `viewer`.
|
||||
* Returns a request handle (caller owns it, must free) or NULL on failure.
|
||||
*/
|
||||
OAKENGINE_API OakEnginePreviewRequest *
|
||||
oakengine_preview_request_single_frame(OakEngineNode *viewer, int64_t num,
|
||||
int64_t den, int dry);
|
||||
|
||||
/**
|
||||
* @brief Request an audio range (num/den seconds in/out) from `viewer`.
|
||||
* Returns a request handle (caller owns it, must free) or NULL on failure.
|
||||
*/
|
||||
OAKENGINE_API OakEnginePreviewRequest *
|
||||
oakengine_preview_request_audio_range(OakEngineNode *viewer, int64_t in_num,
|
||||
int64_t in_den, int64_t out_num,
|
||||
int64_t out_den);
|
||||
|
||||
/** @brief 1 if the request is done, 0 otherwise. 0 on NULL. */
|
||||
OAKENGINE_API int oakengine_preview_request_is_done(
|
||||
const OakEnginePreviewRequest *req);
|
||||
|
||||
/** @brief 1 if the request has a result, 0 otherwise. 0 on NULL. */
|
||||
OAKENGINE_API int oakengine_preview_request_has_result(
|
||||
const OakEnginePreviewRequest *req);
|
||||
|
||||
/** @brief Set a finished callback (called when the ticket completes).
|
||||
* `callback` receives `user_data`. Returns OAKENGINE_E_INVALID on NULL
|
||||
* request. */
|
||||
OAKENGINE_API int oakengine_preview_request_set_finished_callback(
|
||||
OakEnginePreviewRequest *req, void (*callback)(void *),
|
||||
void *user_data);
|
||||
|
||||
/** @brief Copy the frame data into `out`. Returns OAKENGINE_OK or
|
||||
* OAKENGINE_E_INVALID when the request has no video frame result. */
|
||||
OAKENGINE_API int oakengine_preview_request_get_frame(
|
||||
OakEnginePreviewRequest *req, oak_playback_frame *out);
|
||||
|
||||
/** @brief Number of audio channels in the result, or 0 if none. */
|
||||
OAKENGINE_API int oakengine_preview_request_get_audio_channel_count(
|
||||
const OakEnginePreviewRequest *req);
|
||||
|
||||
/** @brief Sample rate of the audio result, or 0 if none. */
|
||||
OAKENGINE_API int oakengine_preview_request_get_audio_sample_rate(
|
||||
const OakEnginePreviewRequest *req);
|
||||
|
||||
/**
|
||||
* @brief Get audio sample data from the result.
|
||||
* `channel` is the 0-based channel index. Writes up to `max_samples` float
|
||||
* values into `samples`. Returns the number of samples written, or
|
||||
* OAKENGINE_E_INVALID on bad arguments.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_preview_request_get_audio_samples(
|
||||
OakEnginePreviewRequest *req, int channel, const float *samples,
|
||||
int max_samples);
|
||||
|
||||
/** @brief Free a preview request handle (NULL-safe). */
|
||||
OAKENGINE_API void oakengine_preview_request_free(
|
||||
OakEnginePreviewRequest *req);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
|
||||
/* Forward declarations from node.h (included by callers in either order). */
|
||||
typedef struct OakEngineNode OakEngineNode;
|
||||
|
||||
/* Forward declaration for playback cache from viewer.h. */
|
||||
typedef struct OakEnginePlaybackCache OakEnginePlaybackCache;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -195,6 +201,127 @@ oakengine_project_sequence_count(const OakEngineProject *self);
|
||||
OAKENGINE_API OakEngineSequence *
|
||||
oakengine_project_sequence_at(const OakEngineProject *self, int index);
|
||||
|
||||
/* ---- Folder operations ---------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Create a folder node named `name` under `parent` in `project`.
|
||||
* Returns a borrowed handle, or NULL on failure.
|
||||
*/
|
||||
OAKENGINE_API OakEngineNode *oakengine_folder_create(OakEngineProject *project,
|
||||
OakEngineNode *parent,
|
||||
const char *name);
|
||||
|
||||
/**
|
||||
* @brief 1 if `folder` recursively contains `child`, 0 otherwise.
|
||||
* 0 when either handle is NULL or `folder` is not a Folder.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_folder_has_child_recursive(
|
||||
const OakEngineNode *folder, const OakEngineNode *child);
|
||||
|
||||
/**
|
||||
* @brief Index of `child` in `folder`'s direct children, or
|
||||
* OAKENGINE_E_NOT_FOUND. Returns OAKENGINE_E_INVALID when
|
||||
* `folder` is not a Folder node.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_folder_index_of_child(
|
||||
const OakEngineNode *folder, const OakEngineNode *child);
|
||||
|
||||
/**
|
||||
* @brief Static input key string for Folder children (Folder::k_child_input).
|
||||
* Never freed.
|
||||
*/
|
||||
OAKENGINE_API const char *oakengine_folder_child_input_key(void);
|
||||
|
||||
/**
|
||||
* @brief Add `child` to `folder` (undoable). OAKENGINE_E_INVALID when
|
||||
* `folder` is not a Folder node or on NULL args.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_folder_add_child(OakEngineNode *folder,
|
||||
OakEngineNode *child);
|
||||
|
||||
/**
|
||||
* @brief Move `node` from its current folder to `new_folder` (undoable).
|
||||
* Removes the node from its old folder first — a true move, not a copy.
|
||||
* Returns OAKENGINE_OK or a negative error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_folder_move_child(OakEngineNode *node,
|
||||
OakEngineNode *new_folder);
|
||||
|
||||
/**
|
||||
* @brief Create a Folder::RemoveElementCommand as an opaque command pointer.
|
||||
* Returns NULL on invalid arguments.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_folder_remove_element_command(
|
||||
OakEngineNode *folder, OakEngineNode *child);
|
||||
|
||||
/**
|
||||
* @brief Move several nodes into `dest_folder` as ONE undoable command
|
||||
* (each node is removed from its old folder, then added to `dest_folder`).
|
||||
* Nodes already directly inside `dest_folder` are skipped. `undo_name`
|
||||
* may be NULL. Returns OAKENGINE_OK or a negative error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_folder_move_children(
|
||||
OakEngineNode *const *nodes, int count, OakEngineNode *dest_folder,
|
||||
const char *undo_name);
|
||||
|
||||
/* ---- Project extras ------------------------------------------------------- */
|
||||
|
||||
/** @brief Root folder node of the project (Project::root()). */
|
||||
OAKENGINE_API OakEngineNode *oakengine_project_root(OakEngineProject *self);
|
||||
|
||||
/** @brief Display name for the project that is safe for window titles
|
||||
* (Project::pretty_filename()). buf/size convention. */
|
||||
OAKENGINE_API int oakengine_project_pretty_filename(const OakEngineProject *self,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/** @brief Set the project's filename (Project::set_filename()).
|
||||
* Returns OAKENGINE_OK or OAKENGINE_E_INVALID on NULL. */
|
||||
OAKENGINE_API int oakengine_project_set_filename(OakEngineProject *self,
|
||||
const char *path);
|
||||
|
||||
/** @brief The project's default cache directory (Project::cache_path()).
|
||||
* buf/size convention. */
|
||||
OAKENGINE_API int oakengine_project_cache_path(const OakEngineProject *self,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/** @brief The project's alongside cache directory
|
||||
* (Project::cache_alongside_path()). buf/size convention. */
|
||||
OAKENGINE_API int oakengine_project_cache_alongside_path(
|
||||
const OakEngineProject *self, char *buf, int buf_size);
|
||||
|
||||
/** @brief Set a custom cache directory path (Project::set_custom_cache_path()).
|
||||
* NULL clears it. */
|
||||
OAKENGINE_API int oakengine_project_set_custom_cache_path(
|
||||
OakEngineProject *self, const char *path);
|
||||
|
||||
/** @brief Get the custom cache directory path, or "" when none is set.
|
||||
* buf/size convention; returns 0 when no custom path is set. */
|
||||
OAKENGINE_API int oakengine_project_get_custom_cache_path(
|
||||
const OakEngineProject *self, char *buf, int buf_size);
|
||||
|
||||
/** @brief Cache location setting enum value
|
||||
* (Project::get_cache_location_setting()). Returns < 0 on NULL. */
|
||||
OAKENGINE_API int oakengine_project_get_cache_location_setting(
|
||||
const OakEngineProject *self);
|
||||
|
||||
/** @brief Static MIME type string for project items (Project::item_mime_type()).
|
||||
* Never freed. */
|
||||
OAKENGINE_API const char *oakengine_project_item_mime_type(void);
|
||||
|
||||
/** @brief Resolve a project node to its owning OakEngineProject
|
||||
* (Project::get_project_from_object()). Returns NULL when the node is
|
||||
* not part of a project or on NULL input. */
|
||||
OAKENGINE_API OakEngineProject *
|
||||
oakengine_project_from_object(const OakEngineNode *node);
|
||||
|
||||
/** @brief Get the project's color reference space name (buf/size). */
|
||||
OAKENGINE_API int oakengine_project_get_color_reference_space(
|
||||
const OakEngineProject *self, char *buf, int buf_size);
|
||||
|
||||
/** @brief Set the project's color reference space (undoable). */
|
||||
OAKENGINE_API int oakengine_project_set_color_reference_space(
|
||||
OakEngineProject *self, const char *colorspace);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/***
|
||||
|
||||
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_PROXY_H
|
||||
#define OAKENGINE_PROXY_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
#include "footage.h"
|
||||
#include "init.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file proxy.h
|
||||
* @brief C ABI for the engine's proxy generation singleton (olive::ProxyManager)
|
||||
*
|
||||
* A thin facade over ProxyManager's instance lifecycle, proxy parameter
|
||||
* configuration, proxy state queries and proxy generation. The opaque task
|
||||
* handle returned in oak_proxy_result::task is a borrowed pointer to the
|
||||
* engine's internal ProxyTask; it is intended only for logging and becomes
|
||||
* invalid when the proxy operation finishes.
|
||||
*
|
||||
* Conventions match the other facade families:
|
||||
* - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
|
||||
* - String output uses the buf/size convention.
|
||||
* - Booleans are int (1/0).
|
||||
*/
|
||||
|
||||
#define OAKENGINE_PROXY_STATE_MISSING 0
|
||||
#define OAKENGINE_PROXY_STATE_GENERATING 1
|
||||
#define OAKENGINE_PROXY_STATE_READY 2
|
||||
#define OAKENGINE_PROXY_STATE_FAILED 3
|
||||
|
||||
typedef struct oak_proxy_result {
|
||||
int state; /**< OAKENGINE_PROXY_STATE_* */
|
||||
char filename[1024];
|
||||
int64_t task; /**< ProxyTask* as opaque handle, or 0 if none */
|
||||
} oak_proxy_result;
|
||||
|
||||
/**
|
||||
* @brief Create the ProxyManager singleton.
|
||||
*
|
||||
* Safe to call when the instance already exists (no-op). Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_FAILED.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_proxy_create_instance(void);
|
||||
|
||||
/**
|
||||
* @brief Destroy the ProxyManager singleton.
|
||||
*
|
||||
* Safe to call when no instance exists (no-op). Returns OAKENGINE_OK.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_proxy_destroy_instance(void);
|
||||
|
||||
/**
|
||||
* @brief Build proxy parameters from the global application config.
|
||||
*
|
||||
* Fills `out` with the configured width/height/divider/version/crf/extension
|
||||
* /preset/include_audio values. Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_proxy_params_from_config(oak_proxy_params *out);
|
||||
|
||||
/**
|
||||
* @brief Query the state of a proxy file on disk.
|
||||
*
|
||||
* Returns one of the OAKENGINE_PROXY_STATE_* values, or
|
||||
* OAKENGINE_PROXY_STATE_MISSING if `proxy_filename` is NULL/empty or the
|
||||
* proxy does not exist.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_proxy_get_state(const char *proxy_filename);
|
||||
|
||||
/**
|
||||
* @brief Human-readable string for a proxy state (buf/size convention).
|
||||
*
|
||||
* Returns the string length on success, or a negative OAKENGINE_E_* code for
|
||||
* an unknown state.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_proxy_state_to_string(int state, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Get or start generating a proxy for `source_filename`.
|
||||
*
|
||||
* `cache_path` is the project cache directory. `stream_index` is the source
|
||||
* stream to proxy. `params` are the proxy generation parameters (width/height
|
||||
* etc.). On return `out->state` and `out->filename` describe the proxy; if a
|
||||
* generation task was started, `out->task` is a borrowed opaque handle to it,
|
||||
* otherwise it is 0.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_proxy_get_or_start(const char *cache_path,
|
||||
const char *source_filename,
|
||||
int stream_index,
|
||||
const oak_proxy_params *params,
|
||||
oak_proxy_result *out);
|
||||
|
||||
/**
|
||||
* @brief Get the "working" filename for a proxy file (buf/size convention).
|
||||
*
|
||||
* The working filename is used by the proxy generator while the proxy is being
|
||||
* generated. Returns the string length on success, or a negative
|
||||
* OAKENGINE_E_* code on error.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_proxy_get_working_filename(const char *proxy_filename,
|
||||
char *buf, int buf_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_PROXY_H */
|
||||
@@ -87,6 +87,45 @@ typedef struct OakEngineFrame OakEngineFrame;
|
||||
*/
|
||||
typedef struct OakEngineAudioBuffer OakEngineAudioBuffer;
|
||||
|
||||
/**
|
||||
* @brief Set aggressive garbage collection on the render manager
|
||||
* (RenderManager::set_aggressive_garbage_collection()). Returns
|
||||
* OAKENGINE_E_STATE when the render manager is not available.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_render_manager_set_aggressive_garbage_collection(int aggressive);
|
||||
|
||||
/**
|
||||
* @brief The render backend that was requested (RenderManager::requested_backend()).
|
||||
* Returns 0 (k_open_gl) when the render manager is not available.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_render_manager_requested_backend(void);
|
||||
|
||||
/**
|
||||
* @brief Convert a render backend enum value to a human-readable string
|
||||
* (RenderManager::backend_to_string()). buf/size convention. Returns the
|
||||
* would-be length or a negative error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_render_manager_backend_to_string(int backend,
|
||||
char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Set the display color processor on the render manager's cacher.
|
||||
* `processor` is a borrowed OakEngineColorProcessor handle (NULL to clear).
|
||||
* Returns OAKENGINE_OK or OAKENGINE_E_STATE.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_render_cache_set_display_color_processor(
|
||||
void *processor);
|
||||
|
||||
/**
|
||||
* @brief Set the multicam node on the render manager's cacher.
|
||||
* `node` is a borrowed OakEngineNode handle (NULL to clear).
|
||||
* Returns OAKENGINE_OK or OAKENGINE_E_STATE.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_render_cache_set_multicam_node(
|
||||
OakEngineNode *node);
|
||||
|
||||
/**
|
||||
* @brief Create a renderer for `seq` producing `width`x`height` frames of
|
||||
* `pixel_format` at the given frame rate.
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/***
|
||||
|
||||
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_SERIALIZER_H
|
||||
#define OAKENGINE_SERIALIZER_H
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "node.h"
|
||||
#include "project.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file serializer.h
|
||||
* @brief C ABI for project serialization / copy-paste (olive::ProjectSerializer)
|
||||
*
|
||||
* A thin facade over ProjectSerializer's load/save/copy/paste primitives. The
|
||||
* opaque OakEngineClipboard handle bundles a SaveData object (for copy/save)
|
||||
* or the LoadData result of the last paste operation. Clipboard handles are
|
||||
* owned by the caller and must be released with oakengine_clipboard_free().
|
||||
*
|
||||
* Conventions match the other facade families:
|
||||
* - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
|
||||
* - String output uses the buf/size convention.
|
||||
*/
|
||||
|
||||
/** @brief Opaque clipboard context. */
|
||||
typedef struct OakEngineClipboard OakEngineClipboard;
|
||||
|
||||
/** @brief Marker handle (defined in oakengine/timeline.h). */
|
||||
typedef struct OakEngineMarker OakEngineMarker;
|
||||
|
||||
#define OAKENGINE_CLIPBOARD_PROJECT 0
|
||||
#define OAKENGINE_CLIPBOARD_NODES 1
|
||||
#define OAKENGINE_CLIPBOARD_CLIPS 2
|
||||
#define OAKENGINE_CLIPBOARD_MARKERS 3
|
||||
#define OAKENGINE_CLIPBOARD_KEYFRAMES 4
|
||||
|
||||
#define OAKENGINE_SERIALIZER_OK 0
|
||||
#define OAKENGINE_SERIALIZER_TOO_OLD 1
|
||||
#define OAKENGINE_SERIALIZER_TOO_NEW 2
|
||||
#define OAKENGINE_SERIALIZER_UNKNOWN_VERSION 3
|
||||
#define OAKENGINE_SERIALIZER_FILE_ERROR 4
|
||||
#define OAKENGINE_SERIALIZER_XML_ERROR 5
|
||||
#define OAKENGINE_SERIALIZER_OVERWRITE_ERROR 6
|
||||
#define OAKENGINE_SERIALIZER_NO_DATA 7
|
||||
|
||||
/**
|
||||
* @brief Returns 1 if `filename` is a compressed project file, 0 otherwise.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_serializer_check_compressed(const char *filename);
|
||||
|
||||
/**
|
||||
* @brief Create a clipboard context for copy/save operations.
|
||||
*
|
||||
* `load_type` is one of OAKENGINE_CLIPBOARD_*. `project` may be NULL for
|
||||
* load types that do not require it. `filename` may be NULL.
|
||||
*/
|
||||
OAKENGINE_API OakEngineClipboard *oakengine_clipboard_create(
|
||||
int load_type, OakEngineProject *project, const char *filename);
|
||||
|
||||
/**
|
||||
* @brief Set the nodes to serialize on this clipboard.
|
||||
*
|
||||
* Replaces any previously set nodes. `nodes` is an array of `count` borrowed
|
||||
* OakEngineNode handles. Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_set_nodes(OakEngineClipboard *cb,
|
||||
const OakEngineNode *const *nodes,
|
||||
int count);
|
||||
|
||||
/**
|
||||
* @brief Set the markers to serialize on this clipboard.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_set_markers(
|
||||
OakEngineClipboard *cb, const OakEngineMarker *const *markers, int count);
|
||||
|
||||
/**
|
||||
* @brief Set the keyframes to serialize on this clipboard.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_set_keyframes(
|
||||
OakEngineClipboard *cb, const OakEngineKeyframe *const *keyframes,
|
||||
int count);
|
||||
|
||||
/**
|
||||
* @brief Set a serialized property attached to a node.
|
||||
*
|
||||
* Properties are free-form (key, value) strings attached to pasted nodes; the
|
||||
* editor uses them for clip in-points/track-refs and node graph positions.
|
||||
* Replaces the value if the same (node, key) pair is set twice. Returns
|
||||
* OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_set_property(OakEngineClipboard *cb,
|
||||
OakEngineNode *node,
|
||||
const char *key,
|
||||
const char *value);
|
||||
|
||||
/**
|
||||
* @brief Copy this clipboard's data to the system clipboard.
|
||||
*
|
||||
* Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_copy(OakEngineClipboard *cb);
|
||||
|
||||
/**
|
||||
* @brief Serialize this clipboard's data to XML (buf/size convention).
|
||||
*
|
||||
* Returns the string length on success, or a negative OAKENGINE_E_* code on
|
||||
* error.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_save_to_xml(OakEngineClipboard *cb,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Paste data from the system clipboard into `project`.
|
||||
*
|
||||
* `load_type` selects what kind of data to paste. On success `*result_code`
|
||||
* receives OAKENGINE_SERIALIZER_OK and the clipboard is populated with the
|
||||
* paste result (accessible through the oakengine_clipboard_get_loaded_*
|
||||
* accessors). On failure `*result_code` receives one of the
|
||||
* OAKENGINE_SERIALIZER_* error codes and a human-readable detail string is
|
||||
* written to `details_buf` (may be NULL). Returns OAKENGINE_OK on success or
|
||||
* an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_paste(OakEngineClipboard *cb,
|
||||
int load_type,
|
||||
OakEngineProject *project,
|
||||
int *result_code,
|
||||
char *details_buf,
|
||||
int details_buf_size);
|
||||
|
||||
/**
|
||||
* @brief Paste data from the system clipboard, invoking `map_fn` for each
|
||||
* original->new node mapping.
|
||||
*
|
||||
* The callback is called once per (original node pointer, pasted node pointer)
|
||||
* pair found in the paste result. The app can use it to build an existing-node
|
||||
* map without exposing C++ containers across the boundary. Returning non-zero
|
||||
* from the callback stops iteration early. Other semantics match
|
||||
* oakengine_clipboard_paste().
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_paste_with_map(
|
||||
OakEngineClipboard *cb, int load_type, OakEngineProject *project,
|
||||
int (*map_fn)(OakEngineNode *old, OakEngineNode *new_node, void *userdata),
|
||||
void *userdata, int *result_code, char *details_buf,
|
||||
int details_buf_size);
|
||||
|
||||
/**
|
||||
* @brief Destroy a clipboard context.
|
||||
*/
|
||||
OAKENGINE_API void oakengine_clipboard_free(OakEngineClipboard *cb);
|
||||
|
||||
/* ---- Paste result accessors (valid after a successful paste) -------------- */
|
||||
|
||||
/**
|
||||
* @brief Number of nodes loaded by the last paste operation.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_get_loaded_node_count(
|
||||
OakEngineClipboard *cb);
|
||||
|
||||
/**
|
||||
* @brief Borrowed node handle loaded at `index`.
|
||||
*/
|
||||
OAKENGINE_API OakEngineNode *oakengine_clipboard_get_loaded_node_at(
|
||||
OakEngineClipboard *cb, int index);
|
||||
|
||||
/**
|
||||
* @brief Number of markers loaded by the last paste operation.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_get_loaded_marker_count(
|
||||
OakEngineClipboard *cb);
|
||||
|
||||
/**
|
||||
* @brief Borrowed marker handle loaded at `index`.
|
||||
*/
|
||||
OAKENGINE_API OakEngineMarker *oakengine_clipboard_get_loaded_marker_at(
|
||||
OakEngineClipboard *cb, int index);
|
||||
|
||||
/**
|
||||
* @brief Number of keyframes loaded by the last paste operation.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_get_loaded_keyframe_count(
|
||||
OakEngineClipboard *cb);
|
||||
|
||||
/**
|
||||
* @brief Borrowed keyframe handle loaded at `index`.
|
||||
*/
|
||||
OAKENGINE_API OakEngineKeyframe *oakengine_clipboard_get_loaded_keyframe_at(
|
||||
OakEngineClipboard *cb, int index);
|
||||
|
||||
/**
|
||||
* @brief Iterate over the serialized properties attached to pasted nodes.
|
||||
*
|
||||
* For each (node, key, value) triple `fn` is called. Returning non-zero stops
|
||||
* iteration early. Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_foreach_property(
|
||||
OakEngineClipboard *cb,
|
||||
int (*fn)(OakEngineNode *node, const char *key, const char *value,
|
||||
void *userdata),
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Iterate over keyframes loaded by the last paste operation.
|
||||
*
|
||||
* For each keyframe `fn` is called with the node id string it belongs to and
|
||||
* the keyframe handle. Returning non-zero stops iteration early. The app can
|
||||
* group keyframes by node id and route them to the correct destination node.
|
||||
* Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_foreach_keyframe(
|
||||
OakEngineClipboard *cb,
|
||||
int (*fn)(const char *node_id, OakEngineKeyframe *keyframe,
|
||||
void *userdata),
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Iterate over promised connections from the paste result.
|
||||
*
|
||||
* For each promised edge `fn` is called with the output node, input node,
|
||||
* input id and element index. Returning non-zero stops iteration early.
|
||||
* Returns OAKENGINE_OK or an error code.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_clipboard_foreach_connection(
|
||||
OakEngineClipboard *cb,
|
||||
int (*fn)(OakEngineNode *output_node, OakEngineNode *input_node,
|
||||
const char *input_id, int element, void *userdata),
|
||||
void *userdata);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_SERIALIZER_H */
|
||||
@@ -0,0 +1,137 @@
|
||||
/***
|
||||
|
||||
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_SYNC_H
|
||||
#define OAKENGINE_SYNC_H
|
||||
|
||||
#include "export.h"
|
||||
#include "timeline.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file sync.h
|
||||
* @brief C ABI for waveform-based audio synchronization estimation
|
||||
*
|
||||
* Wraps the engine's AudioWaveformSync estimators
|
||||
* (engine/audio/audiowaveformsync.h): the full audio of two clips is
|
||||
* rendered through the renderer family and cross-correlated, yielding
|
||||
* the time offset that aligns the target clip with the reference clip
|
||||
* (the application's timeline "synchronize clips by waveform" feature).
|
||||
*
|
||||
* Both estimators validate first and change nothing on failure (these
|
||||
* are pure measurements). They return OAKENGINE_OK when the correlation
|
||||
* is conclusive (OffsetResult::valid), OAKENGINE_E_STATE when it is
|
||||
* inconclusive -- in that case `out_confidence` is still written so the
|
||||
* caller can compare it against a fallback estimator, and the offset
|
||||
* outputs are set to 0 / the stretch output to 1. OAKENGINE_E_INVALID
|
||||
* covers NULL handles, clips without an on-track range, and sequences
|
||||
* without audio; estimation itself requires the engine initialized
|
||||
* with OAKENGINE_INIT_RENDER (OAKENGINE_E_STATE as well).
|
||||
*
|
||||
* Note this family renders audio freshly per call (no waveform-cache
|
||||
* dependency); the application keeps its cache-envelope path for the
|
||||
* envelope source and uses these functions for the estimation step.
|
||||
* Errors follow the family model: per-thread human-readable reason via
|
||||
* oakengine_sync_last_error().
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Estimate the time offset aligning `target` to `reference`
|
||||
* (AudioWaveformSync::estimate_envelope_offset).
|
||||
*
|
||||
* `out_offset_seconds` receives the signed offset in seconds: the
|
||||
* shift to ADD to the target's timeline position so it aligns with the
|
||||
* reference (negative = move the target earlier -- e.g. the
|
||||
* application's AudioSynchronizer adds it to the reference in-point).
|
||||
* The estimate is quantized to the RMS envelope window
|
||||
* (sample_rate/20 seconds), so callers should expect up to one window
|
||||
* of quantization error. `out_confidence` receives the correlation
|
||||
* confidence in [0, 1] and is always written. Any output pointer may
|
||||
* be NULL. Search bounds mirror the application (sample_rate/20
|
||||
* window, 10-minute maximum offset).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sync_estimate_offset(
|
||||
OakEngineSequence *seq, OakEngineClip *reference, OakEngineClip *target,
|
||||
double *out_offset_seconds, double *out_confidence);
|
||||
|
||||
/**
|
||||
* @brief Estimate a playback-rate change plus offset aligning `target`
|
||||
* to `reference` (AudioWaveformSync::estimate_stretch_and_offset).
|
||||
*
|
||||
* `out_stretch` receives the rate the target must be played at to
|
||||
* align (> 1 = the target runs slower and must be sped up; the search
|
||||
* range mirrors the application: 0.75..1.34 in 0.005 steps, 30-second
|
||||
* offset radius). `out_offset_seconds` and `out_confidence` behave
|
||||
* like oakengine_sync_estimate_offset(). Any output pointer may be
|
||||
* NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sync_estimate_stretch_offset(
|
||||
OakEngineSequence *seq, OakEngineClip *reference, OakEngineClip *target,
|
||||
double *out_stretch, double *out_offset_seconds,
|
||||
double *out_confidence);
|
||||
|
||||
/**
|
||||
* @brief Human-readable reason for the last failed sync call on this
|
||||
* thread (buf/size convention). Empty when the last call succeeded.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sync_last_error(char *buf, int buf_size);
|
||||
|
||||
/* ---- Place by source time / waveform offset (replaces AudioSynchronizer) - */
|
||||
|
||||
/** @brief POD for sync placement result (timeline_in rational). */
|
||||
typedef struct oak_sync_placement {
|
||||
int64_t timeline_in_num;
|
||||
int64_t timeline_in_den;
|
||||
} oak_sync_placement;
|
||||
|
||||
/**
|
||||
* @brief Place a clip by source time (AudioSynchronizer::place_by_source_time).
|
||||
*
|
||||
* Computes: timeline_in = anchor_in + (cand_source_start + cand_media_in)
|
||||
* - (ref_source_start + ref_media_in)
|
||||
* Returns OAKENGINE_OK and fills `out`, or OAKENGINE_E_INVALID on NaN input.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sync_place_by_source_time(
|
||||
int64_t ref_source_start_num, int64_t ref_source_start_den,
|
||||
int64_t ref_media_in_num, int64_t ref_media_in_den,
|
||||
int64_t cand_source_start_num, int64_t cand_source_start_den,
|
||||
int64_t cand_media_in_num, int64_t cand_media_in_den,
|
||||
int64_t anchor_num, int64_t anchor_den,
|
||||
oak_sync_placement *out);
|
||||
|
||||
/**
|
||||
* @brief Place a clip by waveform offset (AudioSynchronizer::place_by_waveform_offset).
|
||||
*
|
||||
* Computes: timeline_in = ref_timeline_in + candidate_offset_samples / sample_rate
|
||||
* Returns OAKENGINE_OK and fills `out`, or OAKENGINE_E_INVALID when sample_rate <= 0.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sync_place_by_waveform_offset(
|
||||
int64_t ref_timeline_in_num, int64_t ref_timeline_in_den,
|
||||
int64_t candidate_offset_samples, int sample_rate,
|
||||
oak_sync_placement *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_SYNC_H */
|
||||
@@ -0,0 +1,293 @@
|
||||
/***
|
||||
|
||||
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_TASK_H
|
||||
#define OAKENGINE_TASK_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "encoding.h"
|
||||
#include "init.h"
|
||||
#include "node.h"
|
||||
#include "project.h"
|
||||
#include "timeline.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file task.h
|
||||
* @brief C ABI for the engine background-task system (olive::Task /
|
||||
* olive::TaskManager)
|
||||
*
|
||||
* Tasks are engine objects that run a job (project load/save, footage
|
||||
* import, proxy generation, export) on a worker thread. This family lets
|
||||
* C consumers create the concrete task they need, run it synchronously or
|
||||
* hand it to the global TaskManager queue, and observe its lifecycle
|
||||
* through the event mechanism (oakengine/events.h, task family
|
||||
* OAKENGINE_EVENT_TASK_* and manager family
|
||||
* OAKENGINE_EVENT_TASK_MANAGER_*), without ever seeing the C++ classes.
|
||||
*
|
||||
* Conventions (matching oakengine/project.h):
|
||||
* - OakEngineTask is an opaque borrowed/owned pointer to an
|
||||
* olive::Task subclass.
|
||||
* - A task returned by an oakengine_task_create_*() function is OWNED by
|
||||
* the caller until either oakengine_task_manager_add() (the manager
|
||||
* takes ownership and deletes the task when done) or
|
||||
* oakengine_task_free() (the caller deletes it). A task that ran via
|
||||
* oakengine_task_start_sync() is still owned by the caller and must be
|
||||
* released with oakengine_task_free() (or handed to the manager,
|
||||
* though re-running is unusual).
|
||||
* - Once a task was added to the manager its handle must be treated as
|
||||
* borrowed: the manager may delete it at any time after the
|
||||
* OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED notification.
|
||||
* - Return codes: 0 (OAKENGINE_OK) on success, negative OAKENGINE_E_*
|
||||
* on failure. String output uses the buf/size convention (return value
|
||||
* is the length that would have been written excluding the NUL; a
|
||||
* negative value is an OAKENGINE_E_* error).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Opaque task handle (an olive::Task subclass instance).
|
||||
*/
|
||||
typedef struct OakEngineTask OakEngineTask;
|
||||
|
||||
/* ---- Global task manager ------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Borrowed handle of the global TaskManager singleton, for use as
|
||||
* the subscription handle of the OAKENGINE_EVENT_TASK_MANAGER_* events.
|
||||
* Returns NULL when the engine is not initialized.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_task_manager_handle(void);
|
||||
|
||||
/**
|
||||
* @brief Number of tasks currently known to the manager (running plus
|
||||
* failed-but-kept), or OAKENGINE_E_INVALID when no manager exists.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_manager_count(void);
|
||||
|
||||
/**
|
||||
* @brief Borrowed handle of an arbitrary running task (the manager's
|
||||
* "first" task, used by the status bar), or NULL when the queue is empty
|
||||
* or no manager exists.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTask *oakengine_task_manager_first(void);
|
||||
|
||||
/**
|
||||
* @brief Hand `task` to the global manager queue (takes ownership). The
|
||||
* task starts as soon as a worker thread is available.
|
||||
*
|
||||
* @return OAKENGINE_OK, OAKENGINE_E_INVALID for NULL, OAKENGINE_E_STATE
|
||||
* when no manager exists.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_manager_add(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief Ask the manager to cancel `task` (TaskManager::cancel_task
|
||||
* semantics: a running task is signalled; a failed-but-kept task is
|
||||
* removed and deleted).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_manager_cancel(OakEngineTask *task);
|
||||
|
||||
/* ---- Task accessors ------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* @brief Title of `task` (buf/size convention).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_title(OakEngineTask *task, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Error message of `task` (buf/size convention). Meaningful after a
|
||||
* failed run.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_error(OakEngineTask *task, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Start timestamp of `task` (milliseconds since epoch), 0 when the
|
||||
* task never started, OAKENGINE_E_INVALID for NULL.
|
||||
*/
|
||||
OAKENGINE_API int64_t oakengine_task_start_time(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief 1 when `task` was asked to cancel, 0 otherwise,
|
||||
* OAKENGINE_E_INVALID for NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_is_cancelled(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief Signal `task` to cancel as soon as possible (Task::Cancel).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_cancel(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief Run `task` synchronously on the CALLING thread (Task::start).
|
||||
* Emits the task events on this thread. Ownership stays with the caller.
|
||||
*
|
||||
* @return 1 when the task succeeded, 0 when it failed or was cancelled
|
||||
* (read oakengine_task_error()), OAKENGINE_E_INVALID for NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_start_sync(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief Delete a task that was never added to the manager.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_free(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief Run `task` through the engine's CLI modal progress dialog and return
|
||||
* 1 when it succeeds, 0 when it fails or is cancelled.
|
||||
*
|
||||
* The dialog shows the task's title and progress on the terminal. `parent`
|
||||
* is an optional QObject parent (may be NULL). The task is started
|
||||
* synchronously; the caller retains ownership and must free it with
|
||||
* oakengine_task_free() when done.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_cli_task_dialog_run(OakEngineTask *task,
|
||||
void *parent_or_NULL);
|
||||
|
||||
/* ---- Task creators --------------------------------------------------------
|
||||
*
|
||||
* All creators return an OWNED task (NULL on invalid input). The task is
|
||||
* not started by creation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Task that loads an OVE project from `filename`.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTask *
|
||||
oakengine_task_create_project_load(const char *filename);
|
||||
|
||||
/**
|
||||
* @brief Task that loads an OpenTimelineIO project from `filename`.
|
||||
* Returns NULL when the engine was built without OTIO support.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTask *
|
||||
oakengine_task_create_project_load_otio(const char *filename);
|
||||
|
||||
/**
|
||||
* @brief Task that saves `project` (ProjectSaveTask semantics).
|
||||
*
|
||||
* `use_compression` selects the compressed .ove writer (0 writes the
|
||||
* uncompressed .ovexml form). `override_filename` may be NULL to save to
|
||||
* the project's own filename. `layout` is an opaque
|
||||
* `const olive::SerializedLayoutInfo *` (may be NULL) whose contents are
|
||||
* copied into the saved file.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTask *oakengine_task_create_project_save(
|
||||
OakEngineProject *project, int use_compression,
|
||||
const char *override_filename, const void *layout);
|
||||
|
||||
/**
|
||||
* @brief Task that saves `project` in OpenTimelineIO format. Returns NULL
|
||||
* when the engine was built without OTIO support.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTask *
|
||||
oakengine_task_create_project_save_otio(OakEngineProject *project);
|
||||
|
||||
/**
|
||||
* @brief Task that imports `url_count` media files into `folder` (a folder
|
||||
* node of the target project; use oakengine_project_root() for the top
|
||||
* level). The URL array is copied during the call.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTask *oakengine_task_create_project_import(
|
||||
OakEngineNode *folder, const char **urls, int url_count);
|
||||
|
||||
/**
|
||||
* @brief Task that generates the proxy media for `footage` (a footage node
|
||||
* handle, as accepted by oakengine_footage_borrow(); the task keeps the
|
||||
* underlying node).
|
||||
*/
|
||||
OAKENGINE_API OakEngineTask *
|
||||
oakengine_task_create_proxy(OakEngineNode *footage);
|
||||
|
||||
/**
|
||||
* @brief Task that renders an export of `sequence` with `params`.
|
||||
*
|
||||
* Takes ownership of `params` (destroyed with the task). Progress is
|
||||
* reported through the OAKENGINE_EVENT_TASK_PROGRESS event; cancelling
|
||||
* the task cancels the engine export render.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTask *oakengine_task_create_export(
|
||||
OakEngineSequence *sequence, OakEngineEncodingParams *params);
|
||||
|
||||
/* ---- Import task results --------------------------------------------------
|
||||
*
|
||||
* Valid on a task created by oakengine_task_create_project_import() after
|
||||
* it ran; all return OAKENGINE_E_INVALID (or 0/NULL) for other tasks.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Number of files the import task will process (valid right after
|
||||
* creation; 0 means "nothing to import" and the task should be freed
|
||||
* instead of run).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_import_file_count(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief The undo command built by a successful import run as an opaque
|
||||
* `olive::MultiUndoCommand *` (NULL before the run, after a cancelled
|
||||
* run, or on a second call). Ownership is DETACHED from the task and
|
||||
* passes to the caller: push it with oakengine_undo_push() or delete it.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_task_import_get_command(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief Number of footage items a successful import run created.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_import_footage_count(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief Borrowed node handle of the imported footage item at `index`
|
||||
* (NULL when out of range).
|
||||
*/
|
||||
OAKENGINE_API OakEngineNode *
|
||||
oakengine_task_import_footage_at(OakEngineTask *task, int index);
|
||||
|
||||
/**
|
||||
* @brief Number of files the import task rejected.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_task_import_invalid_files_count(OakEngineTask *task);
|
||||
|
||||
/**
|
||||
* @brief Rejected file path at `index` (buf/size convention).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_task_import_invalid_file_at(OakEngineTask *task,
|
||||
int index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/* ---- Save task results ---------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Borrowed handle of the project a save task wrote (NULL for other
|
||||
* tasks).
|
||||
*/
|
||||
OAKENGINE_API OakEngineProject *
|
||||
oakengine_task_save_get_project(OakEngineTask *task);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_TASK_H */
|
||||
@@ -301,6 +301,48 @@ OAKENGINE_API int oakengine_sequence_marker_at(const OakEngineSequence *self,
|
||||
*/
|
||||
typedef struct OakEngineClip OakEngineClip;
|
||||
|
||||
/**
|
||||
* @brief Opaque marker list handle (olive::TimelineMarkerList).
|
||||
*
|
||||
* Borrowed from oakengine_viewer_get_marker_list(). Invalidated when the
|
||||
* owning viewer is freed.
|
||||
*/
|
||||
typedef struct OakEngineMarkerList OakEngineMarkerList;
|
||||
|
||||
/**
|
||||
* @brief Opaque marker handle (olive::TimelineMarker).
|
||||
*
|
||||
* Borrowed from oakengine_marker_list_at() / oakengine_marker_list_marker_at_time().
|
||||
* Invalidated when the owning project is freed.
|
||||
*/
|
||||
typedef struct OakEngineMarker OakEngineMarker;
|
||||
|
||||
/**
|
||||
* @brief Opaque workarea handle (olive::TimelineWorkArea).
|
||||
*
|
||||
* Borrowed from oakengine_viewer_get_workarea_handle() or created standalone
|
||||
* with oakengine_workarea_create(). Must be freed with oakengine_workarea_free()
|
||||
* when created standalone; borrowed handles are invalidated with their owner.
|
||||
*/
|
||||
typedef struct OakEngineWorkarea OakEngineWorkarea;
|
||||
|
||||
/**
|
||||
* @brief Opaque track handle (olive::Track).
|
||||
*
|
||||
* Borrowed from oakengine_sequence_track_at(). Invalidated when the owning
|
||||
* sequence is freed.
|
||||
*/
|
||||
typedef struct OakEngineTrack OakEngineTrack;
|
||||
|
||||
/**
|
||||
* @brief Opaque block handle (olive::Block).
|
||||
*
|
||||
* A generic block on a track (ClipBlock, GapBlock, TransitionBlock, etc).
|
||||
* Borrowed from events or cast from OakEngineClip* / OakEngineTrack*. The
|
||||
* handle is invalidated when the owning project is freed.
|
||||
*/
|
||||
typedef struct OakEngineBlock OakEngineBlock;
|
||||
|
||||
/**
|
||||
* @brief Human-readable reason for the last failed editing call on this
|
||||
* thread (buf/size convention). Editing calls return NULL or a negative
|
||||
@@ -322,6 +364,41 @@ OAKENGINE_API int oakengine_sequence_last_error(char *buf, int buf_size);
|
||||
OAKENGINE_API int oakengine_sequence_add_track(OakEngineSequence *self,
|
||||
int track_type);
|
||||
|
||||
/**
|
||||
* @brief Create a TimelineAddTrackCommand as an opaque command pointer without
|
||||
* executing or pushing it. If `out_track` is non-NULL, it receives a borrowed
|
||||
* handle to the track that the command will create on redo.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_sequence_add_track_command(
|
||||
OakEngineSequence *self, int track_type, int auto_merge,
|
||||
OakEngineTrack **out_track);
|
||||
|
||||
/** Movement modes for ripple/trim commands (mirror olive::Timeline::MovementMode). */
|
||||
#define OAKENGINE_MOVEMENT_MODE_NONE 0
|
||||
#define OAKENGINE_MOVEMENT_MODE_MOVE 1
|
||||
#define OAKENGINE_MOVEMENT_MODE_TRIM_IN 2
|
||||
#define OAKENGINE_MOVEMENT_MODE_TRIM_OUT 3
|
||||
|
||||
/**
|
||||
* @brief One entry in a TrackListRippleToolCommand hash: the track to ripple,
|
||||
* the block being moved, and whether a gap should be appended after it.
|
||||
*/
|
||||
typedef struct oakengine_ripple_info {
|
||||
OakEngineTrack *track;
|
||||
OakEngineBlock *block;
|
||||
int append_gap;
|
||||
} oakengine_ripple_info;
|
||||
|
||||
/**
|
||||
* @brief Create a TrackListRippleToolCommand as an opaque command pointer.
|
||||
* `infos` holds one entry per affected track; `movement` is a rational offset
|
||||
* in seconds. Returns NULL on invalid arguments.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_sequence_ripple_tracks_command(
|
||||
OakEngineSequence *self, int track_type,
|
||||
const oakengine_ripple_info *infos, int info_count,
|
||||
int64_t movement_num, int64_t movement_den, int movement_mode);
|
||||
|
||||
/**
|
||||
* @brief Place a clip of `footage` on a track (undoable).
|
||||
*
|
||||
@@ -374,6 +451,15 @@ OAKENGINE_API int oakengine_clip_get_range(const OakEngineClip *self,
|
||||
int64_t *in, int64_t *out,
|
||||
int64_t *media_in);
|
||||
|
||||
/**
|
||||
* @brief The sequence that owns the clip's track.
|
||||
*
|
||||
* Returns a borrowed handle (the clip's track's parent sequence) or NULL if
|
||||
* the clip is not on a track.
|
||||
*/
|
||||
OAKENGINE_API OakEngineSequence *oakengine_clip_get_sequence(
|
||||
const OakEngineClip *self);
|
||||
|
||||
/* ---- Editing primitives, round 2: split / ripple delete / trim / move ----
|
||||
*
|
||||
* All four are undoable like the other editing primitives and report
|
||||
@@ -712,6 +798,384 @@ OAKENGINE_API int oakengine_sequence_marker_rename(OakEngineSequence *seq,
|
||||
int64_t time_ts,
|
||||
const char *name);
|
||||
|
||||
/* ---- Marker handle family ----------------------------------------------------
|
||||
*
|
||||
* Marker list and individual marker operations on opaque handles. The list
|
||||
* is obtained from oakengine_viewer_get_marker_list() (declared in viewer.h).
|
||||
* These functions operate on the handle level rather than through the sequence,
|
||||
* for fine-grained undo/redo and direct marker manipulation.
|
||||
*
|
||||
* All times are rational seconds (numerator/denominator pairs) matching the
|
||||
* engine's internal time representation.
|
||||
*/
|
||||
|
||||
/** @brief Number of markers in the list. 0 on a NULL handle. */
|
||||
OAKENGINE_API int oakengine_marker_list_count(const OakEngineMarkerList *list);
|
||||
|
||||
/** @brief Add a marker with the given rational time range, name, and color.
|
||||
* Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_marker_list_add(OakEngineMarkerList *list,
|
||||
int64_t in_num, int64_t in_den,
|
||||
int64_t out_num, int64_t out_den,
|
||||
const char *name, int color);
|
||||
|
||||
/**
|
||||
* @brief Create a detached marker (not yet added to any list).
|
||||
*
|
||||
* The returned handle can be passed to MarkerPropertiesDialog, then either
|
||||
* added with oakengine_marker_list_add_existing() or freed with
|
||||
* oakengine_marker_free().
|
||||
*/
|
||||
OAKENGINE_API OakEngineMarker *oakengine_marker_create(
|
||||
int color, int64_t in_num, int64_t in_den, int64_t out_num, int64_t out_den,
|
||||
const char *name);
|
||||
|
||||
/** @brief Free a detached marker created by oakengine_marker_create(). */
|
||||
OAKENGINE_API void oakengine_marker_free(OakEngineMarker *marker);
|
||||
|
||||
/** @brief Re-add an existing (removed) marker to the list. Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_marker_list_add_existing(OakEngineMarkerList *list,
|
||||
OakEngineMarker *marker);
|
||||
|
||||
/** @brief Marker at the given sorted index, or NULL if out of range. */
|
||||
OAKENGINE_API OakEngineMarker *
|
||||
oakengine_marker_list_at(const OakEngineMarkerList *list, int index);
|
||||
|
||||
/** @brief Find a marker by its exact in-point time (rational seconds).
|
||||
* Returns the marker or NULL if not found. */
|
||||
OAKENGINE_API OakEngineMarker *
|
||||
oakengine_marker_list_marker_at_time(const OakEngineMarkerList *list,
|
||||
int64_t num, int64_t den);
|
||||
|
||||
/** @brief Get the marker's time range as rational seconds. Any pointer
|
||||
* may be NULL. Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_marker_get_time(const OakEngineMarker *self,
|
||||
int64_t *in_num, int64_t *in_den,
|
||||
int64_t *out_num,
|
||||
int64_t *out_den);
|
||||
|
||||
/** @brief Get the marker's name (buf/size convention). Returns the
|
||||
* would-be length (excluding NUL) or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_marker_get_name(const OakEngineMarker *self,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/** @brief Get the marker's color index. Returns -1 on a NULL handle. */
|
||||
OAKENGINE_API int oakengine_marker_get_color(const OakEngineMarker *self);
|
||||
|
||||
/** @brief 1 if the marker list has another marker at the given rational
|
||||
* time, 0 otherwise. 0 on a NULL marker handle. */
|
||||
OAKENGINE_API int
|
||||
oakengine_marker_has_sibling_at_time(const OakEngineMarker *self, int64_t num,
|
||||
int64_t den);
|
||||
|
||||
/** @brief Set the marker's time range live (non-undoable). Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_marker_set_time_live(OakEngineMarker *self,
|
||||
int64_t in_num,
|
||||
int64_t in_den,
|
||||
int64_t out_num,
|
||||
int64_t out_den);
|
||||
|
||||
/** @brief Commit a time change as an undoable command (undo restores the
|
||||
* pre-commit state). Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_marker_commit_time(
|
||||
OakEngineMarker *self, int64_t old_in_num, int64_t old_in_den,
|
||||
int64_t old_out_num, int64_t old_out_den, int64_t new_in_num,
|
||||
int64_t new_in_den, int64_t new_out_num, int64_t new_out_den,
|
||||
void *command);
|
||||
|
||||
/**
|
||||
* @brief Create a MarkerChangeTimeCommand as an opaque command pointer.
|
||||
* `new_time_num`/`new_time_den` is the new in-point in rational seconds;
|
||||
* the marker's out-point offset is preserved.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_marker_set_time_command(
|
||||
OakEngineMarker *marker, int64_t new_time_num, int64_t new_time_den);
|
||||
|
||||
/** @brief Remove the marker from its list (undoable). Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_marker_remove(OakEngineMarker *self);
|
||||
|
||||
/** @brief Batch-set properties on one or more markers (undoable, ONE
|
||||
* command). Pass -1 for color to leave it unchanged; pass NULL for name
|
||||
* to leave it unchanged. When `count` == 1, optionally move the marker's
|
||||
* time range. Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_marker_set_properties(
|
||||
OakEngineMarker **markers, int count, int color, const char *name,
|
||||
int move_time, int64_t new_in_num, int64_t new_in_den,
|
||||
int64_t new_out_num, int64_t new_out_den, void *command);
|
||||
|
||||
/* ---- Workarea handle family ---------------------------------------------------
|
||||
*
|
||||
* Workarea operations on opaque OakEngineWorkarea handles. Create with
|
||||
* oakengine_workarea_create() or borrow from a viewer with
|
||||
* oakengine_viewer_get_workarea_handle(). Standalone workareas must be
|
||||
* freed with oakengine_workarea_free(). All times are rational seconds.
|
||||
*/
|
||||
|
||||
/** @brief Create a standalone workarea (caller owns it). */
|
||||
OAKENGINE_API OakEngineWorkarea *oakengine_workarea_create(void);
|
||||
|
||||
/** @brief Free a standalone workarea. NULL-safe. */
|
||||
OAKENGINE_API void oakengine_workarea_free(OakEngineWorkarea *wa);
|
||||
|
||||
/** @brief Read the workarea state. Any pointer may be NULL. Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_workarea_get(const OakEngineWorkarea *self,
|
||||
int64_t *in_num, int64_t *in_den,
|
||||
int64_t *out_num, int64_t *out_den,
|
||||
int *enabled);
|
||||
|
||||
/** @brief Set the workarea range (non-undoable). Returns OAKENGINE_OK or
|
||||
* OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_workarea_set_range(OakEngineWorkarea *self,
|
||||
int64_t in_num, int64_t in_den,
|
||||
int64_t out_num, int64_t out_den);
|
||||
|
||||
/** @brief Enable/disable the workarea (non-undoable). Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_workarea_set_enabled(OakEngineWorkarea *self,
|
||||
int enabled);
|
||||
|
||||
/** @brief Set the workarea range with undo support. Pass the reset
|
||||
* sentinels (from oakengine_workarea_reset_in_out()) for the old range
|
||||
* when creating fresh. Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_workarea_set_range_undoable(
|
||||
OakEngineWorkarea *self, int64_t in_num, int64_t in_den, int64_t out_num,
|
||||
int64_t out_den, int64_t old_in_num, int64_t old_in_den,
|
||||
int64_t old_out_num, int64_t old_out_den, void *command);
|
||||
|
||||
/** @brief Enable/disable the workarea with undo support. Pass NULL for
|
||||
* command (creates a standalone undo command that is pushed onto the
|
||||
* global stack when the workarea has an owning project). Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_workarea_set_enabled_undoable(
|
||||
OakEngineWorkarea *self, int enabled, void *command);
|
||||
|
||||
/** @brief Fill the reset sentinel values (in = 0/1, out = RATIONAL_MAX).
|
||||
* Any pointer may be NULL. */
|
||||
OAKENGINE_API void oakengine_workarea_reset_in_out(int64_t *in_num,
|
||||
int64_t *in_den,
|
||||
int64_t *out_num,
|
||||
int64_t *out_den);
|
||||
|
||||
/* ---- Clip media range / cache / media in --------------------------------- */
|
||||
|
||||
/** @brief Get the clip's media range as rational seconds
|
||||
* (ClipBlock::media_range()). Any pointer may be NULL. Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_clip_get_media_range_rational(
|
||||
const OakEngineClip *self, int64_t *in_num, int64_t *in_den,
|
||||
int64_t *out_num, int64_t *out_den);
|
||||
|
||||
/** @brief Get the clip's media in-point as rational seconds
|
||||
* (ClipBlock::media_in()). Any pointer may be NULL. Returns OAKENGINE_OK
|
||||
* or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_clip_get_media_in_rational(
|
||||
const OakEngineClip *self, int64_t *num, int64_t *den);
|
||||
|
||||
/** @brief Move the clip's media in-point (undoable when undoable != 0,
|
||||
* else direct). Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_clip_set_media_in(OakEngineClip *self,
|
||||
int64_t media_in_ts,
|
||||
int undoable);
|
||||
|
||||
/** @brief Move the clip's media in-point as a rational seconds value
|
||||
* (undoable when undoable != 0, else direct). This variant does not
|
||||
* require the clip to be on a track yet. Returns OAKENGINE_OK or
|
||||
* OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_clip_set_media_in_rational(OakEngineClip *self,
|
||||
int64_t num,
|
||||
int64_t den,
|
||||
int undoable);
|
||||
|
||||
/** @brief Request invalidation of the cache for the given range. NULL-safe
|
||||
* (no-op). */
|
||||
OAKENGINE_API void oakengine_clip_request_invalidate(OakEngineClip *self,
|
||||
int64_t in_ts,
|
||||
int64_t out_ts,
|
||||
int type);
|
||||
|
||||
/** @brief Add a cache passthrough dependency (copy results from `source`
|
||||
* to `dest`). NULL-safe (no-op). */
|
||||
OAKENGINE_API void oakengine_clip_add_cache_passthrough(
|
||||
OakEngineClip *dest, OakEngineClip *source);
|
||||
|
||||
/** @brief Discard the clip's cache. NULL-safe (no-op). */
|
||||
OAKENGINE_API void oakengine_clip_discard_cache(OakEngineClip *self);
|
||||
|
||||
/** @brief Create a new empty ClipBlock. The caller owns the returned node
|
||||
* and must add it to a project (e.g. via oakengine_project_add_node or a
|
||||
* custom undo command) before the engine can manage its lifecycle. The
|
||||
* optional `label` sets the node's user label (Node::set_label()). */
|
||||
OAKENGINE_API OakEngineClip *oakengine_clip_create_empty(const char *label);
|
||||
|
||||
/** @brief Request invalidated cache ranges from the node connected to the
|
||||
* clip's buffer input (ClipBlock::request_invalidated_from_connected()).
|
||||
* Pass in_den == 0 or out_den == 0 to intersect the full media range. */
|
||||
OAKENGINE_API void oakengine_clip_request_invalidate_connected(
|
||||
OakEngineClip *self, int force_all, int64_t in_num, int64_t in_den,
|
||||
int64_t out_num, int64_t out_den);
|
||||
|
||||
/* ---- Block functions (generic block, not just ClipBlock) ------------------ */
|
||||
|
||||
/** @brief 1 if the block is enabled (Block::is_enabled()). 0 on NULL. */
|
||||
OAKENGINE_API int oakengine_block_is_enabled(const OakEngineBlock *self);
|
||||
|
||||
/** @brief Enable or disable the block (undoable). Returns OAKENGINE_OK
|
||||
* or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_block_set_enabled(OakEngineBlock *self,
|
||||
int enabled);
|
||||
|
||||
/* ---- Block traversal -------------------------------------------------------- */
|
||||
|
||||
/** @brief Number of blocks (including gaps) on the track. Returns
|
||||
* OAKENGINE_E_INVALID for a NULL handle. */
|
||||
OAKENGINE_API int oakengine_track_block_count(const OakEngineTrack *track);
|
||||
|
||||
/** @brief The block at `index` on the track (0-based, includes gaps).
|
||||
* Returns NULL when out of range or on a NULL handle. */
|
||||
OAKENGINE_API OakEngineBlock *
|
||||
oakengine_track_block_at(const OakEngineTrack *track, int index);
|
||||
|
||||
/** @brief The block at the given timestamp, or NULL if the time falls in
|
||||
* a gap or past the end. Timestamp is in the track's sequence timebase. */
|
||||
OAKENGINE_API OakEngineBlock *
|
||||
oakengine_track_block_at_time(const OakEngineTrack *track, int64_t timestamp);
|
||||
|
||||
/** @brief Nearest block whose out-point is strictly before `timestamp`.
|
||||
* Returns NULL when none. */
|
||||
OAKENGINE_API OakEngineBlock *
|
||||
oakengine_track_nearest_block_before(const OakEngineTrack *track,
|
||||
int64_t timestamp);
|
||||
|
||||
/** @brief Nearest block whose in-point is strictly after `timestamp`.
|
||||
* Returns NULL when none. */
|
||||
OAKENGINE_API OakEngineBlock *
|
||||
oakengine_track_nearest_block_after(const OakEngineTrack *track,
|
||||
int64_t timestamp);
|
||||
|
||||
/** @brief Nearest block whose out-point >= `timestamp`
|
||||
* (i.e. the block containing or immediately before the time).
|
||||
* Returns NULL when none. */
|
||||
OAKENGINE_API OakEngineBlock *
|
||||
oakengine_track_nearest_block_before_or_at(const OakEngineTrack *track,
|
||||
int64_t timestamp);
|
||||
|
||||
/** @brief Nearest block whose in-point <= `timestamp`
|
||||
* (i.e. the block containing or immediately after the time).
|
||||
* Returns NULL when none. */
|
||||
OAKENGINE_API OakEngineBlock *
|
||||
oakengine_track_nearest_block_after_or_at(const OakEngineTrack *track,
|
||||
int64_t timestamp);
|
||||
|
||||
/** @brief 1 if the block is a GapBlock, 0 otherwise. 0 on NULL. */
|
||||
OAKENGINE_API int oakengine_block_is_gap(const OakEngineBlock *block);
|
||||
|
||||
/** @brief Next block in the track's linked list, or NULL. NULL on NULL. */
|
||||
OAKENGINE_API OakEngineBlock *oakengine_block_next(const OakEngineBlock *block);
|
||||
|
||||
/** @brief Previous block in the track's linked list, or NULL. NULL on NULL. */
|
||||
OAKENGINE_API OakEngineBlock *oakengine_block_prev(const OakEngineBlock *block);
|
||||
|
||||
/** @brief Fill `in` and `out` with the block's range as timestamps in the
|
||||
* owning track's sequence timebase. Either pointer may be NULL. Returns
|
||||
* OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_block_get_range(const OakEngineBlock *block,
|
||||
int64_t *in, int64_t *out);
|
||||
|
||||
/* ---- Clip input ID getters ------------------------------------------------- */
|
||||
|
||||
/** @brief ClipBlock::k_buffer_in. Static string, never freed. */
|
||||
OAKENGINE_API const char *oakengine_clip_buffer_input_id(void);
|
||||
/** @brief ClipBlock::k_speed_input. */
|
||||
OAKENGINE_API const char *oakengine_clip_speed_input_id(void);
|
||||
/** @brief ClipBlock::k_reverse_input. */
|
||||
OAKENGINE_API const char *oakengine_clip_reverse_input_id(void);
|
||||
/** @brief ClipBlock::k_maintain_audio_pitch_input. */
|
||||
OAKENGINE_API const char *
|
||||
oakengine_clip_maintain_audio_pitch_input_id(void);
|
||||
/** @brief ClipBlock::k_loop_mode_input. */
|
||||
OAKENGINE_API const char *oakengine_clip_loop_mode_input_id(void);
|
||||
/** @brief ClipBlock::k_auto_cache_input. */
|
||||
OAKENGINE_API const char *oakengine_clip_auto_cache_input_id(void);
|
||||
|
||||
/* ---- Sequence: add_default_nodes ------------------------------------------ */
|
||||
|
||||
/** @brief Add one video and one audio track as ONE undoable command
|
||||
* (ViewerOutput helper used by the application). Returns OAKENGINE_OK
|
||||
* or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int
|
||||
oakengine_sequence_add_default_nodes(OakEngineSequence *seq);
|
||||
|
||||
/* ---- Sequence: add_sequence_clip ------------------------------------------- */
|
||||
|
||||
/** @brief Place a nested Sequence as a clip on a track (undoable).
|
||||
*
|
||||
* Same semantics as oakengine_sequence_add_footage_clip() but creates a
|
||||
* clip whose buffer input feeds from another Sequence node (nested
|
||||
* timeline). Self-nesting and circular nesting are detected and rejected.
|
||||
* Returns a borrowed clip handle or NULL on failure. */
|
||||
OAKENGINE_API OakEngineClip *
|
||||
oakengine_sequence_add_sequence_clip(OakEngineSequence *seq,
|
||||
OakEngineSequence *nested,
|
||||
int track_type, int track_index,
|
||||
int64_t in, int64_t out,
|
||||
int64_t media_in);
|
||||
|
||||
/* ---- Track handle queries -------------------------------------------------- */
|
||||
|
||||
/** @brief Borrowed track handle, or NULL if the track does not exist. */
|
||||
OAKENGINE_API OakEngineTrack *
|
||||
oakengine_sequence_track_at(const OakEngineSequence *seq, int track_type,
|
||||
int track_index);
|
||||
|
||||
/** @brief Track type (OAKENGINE_TRACK_TYPE_*), or -1 on a NULL handle. */
|
||||
OAKENGINE_API int oakengine_track_type(const OakEngineTrack *track);
|
||||
|
||||
/** @brief Track content length in frame timestamps (Track::get_length()
|
||||
* converted to timebase units). Returns OAKENGINE_OK or
|
||||
* OAKENGINE_E_NOT_FOUND/OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_track_get_length(const OakEngineSequence *seq,
|
||||
int track_type, int track_index,
|
||||
int64_t *length);
|
||||
|
||||
/** @brief 1 if the range [in_ts, out_ts) is free (no blocks intersect it),
|
||||
* 0 if it intersects. Returns OAKENGINE_E_NOT_FOUND when the track does
|
||||
* not exist, OAKENGINE_E_INVALID for bad arguments. */
|
||||
OAKENGINE_API int oakengine_track_is_range_free(const OakEngineSequence *seq,
|
||||
int track_type,
|
||||
int track_index,
|
||||
int64_t in_ts, int64_t out_ts);
|
||||
|
||||
/** @brief Track height helpers (matching the engine's
|
||||
* Track::k_height_* constants). */
|
||||
OAKENGINE_API double oakengine_track_height_default(void);
|
||||
OAKENGINE_API int oakengine_track_default_height_in_pixels(void);
|
||||
OAKENGINE_API int oakengine_track_height_internal_to_pixels(double height);
|
||||
OAKENGINE_API double oakengine_track_height_pixels_to_internal(int pixels);
|
||||
|
||||
/** @brief Track height step interval (e.g. 0.5). > 0.0. */
|
||||
OAKENGINE_API double oakengine_track_height_interval(void);
|
||||
|
||||
/** @brief Minimum track height (e.g. 1.5). > 0.0. */
|
||||
OAKENGINE_API double oakengine_track_height_minimum(void);
|
||||
|
||||
/* ---- Multicam helpers --------------------------------------------------- */
|
||||
|
||||
/** @brief Find the MultiCamNode ancestor of a clip, or NULL. Accepts
|
||||
* OakEngineNode* (a clip or any node). */
|
||||
OAKENGINE_API OakEngineNode *
|
||||
oakengine_clip_find_multicam(OakEngineNode *node);
|
||||
|
||||
/** @brief Switch the multicam source to the given track/stream at the
|
||||
* given time. Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
|
||||
OAKENGINE_API int oakengine_multicam_switch_source(
|
||||
OakEngineNode *multicam_node, OakEngineNode *footage_node,
|
||||
int track_type, int track_index, double time_seconds,
|
||||
void *command);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/***
|
||||
|
||||
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_TRAVERSE_H
|
||||
#define OAKENGINE_TRAVERSE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "node.h"
|
||||
#include "videoparams.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file traverse.h
|
||||
* @brief C ABI for synchronous node-graph value evaluation
|
||||
* (olive::NodeTraverser)
|
||||
*
|
||||
* The engine evaluates a node's value at a given time by traversing its
|
||||
* input graph (olive::NodeTraverser). The application uses this in three
|
||||
* places: the node table view (per-input value databases), the node value
|
||||
* tree (one output table + the element a downstream input's value hint
|
||||
* selects) and the viewer display gizmos (a transform between two nodes
|
||||
* and the gizmo node's input row at drag start). This family exposes
|
||||
* those paths without leaking NodeValueTable/NodeValueRow C++ types.
|
||||
*
|
||||
* All evaluation is SYNCHRONOUS on the calling thread and CPU-only in
|
||||
* this family (textures are resolved as engine-side dummy textures, which
|
||||
* is exactly what the table/tree views need -- they only read metadata).
|
||||
* Call from the GUI thread.
|
||||
*
|
||||
* OakEngineTraverseDb is an OWNED result object; free it with
|
||||
* oakengine_traverse_db_free(). Strings it returns point into the object
|
||||
* and are valid until freed. Times are Rational seconds as int64
|
||||
* numerator/denominator pairs, like the rest of the facade.
|
||||
*/
|
||||
|
||||
typedef struct OakEngineTraverseDb OakEngineTraverseDb;
|
||||
|
||||
/**
|
||||
* @brief Evaluate every input of `node` over [in_num/in_den,
|
||||
* out_num/out_den] seconds and return the per-input value database
|
||||
* (NodeTraverser::generate_database()). NULL on invalid arguments.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTraverseDb *oakengine_traverse_generate_database(
|
||||
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
|
||||
int64_t out_den);
|
||||
|
||||
/**
|
||||
* @brief Evaluate the single output table of `node`
|
||||
* (NodeTraverser::generate_table()). Returned as a database with exactly
|
||||
* one entry whose input id is an empty string.
|
||||
*/
|
||||
OAKENGINE_API OakEngineTraverseDb *oakengine_traverse_generate_table(
|
||||
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
|
||||
int64_t out_den);
|
||||
|
||||
/** @brief Free a database returned by this family. NULL is a no-op. */
|
||||
OAKENGINE_API void oakengine_traverse_db_free(OakEngineTraverseDb *db);
|
||||
|
||||
/** @brief Number of input entries (generate_database: one per input id
|
||||
* that produced a table; generate_table: exactly 1). */
|
||||
OAKENGINE_API int oakengine_traverse_db_input_count(
|
||||
const OakEngineTraverseDb *db);
|
||||
|
||||
/** @brief Input id of entry `input_index` (valid until db is freed). */
|
||||
OAKENGINE_API const char *oakengine_traverse_db_input_id(
|
||||
const OakEngineTraverseDb *db, int input_index);
|
||||
|
||||
/** @brief Row count of the table at `input_index`
|
||||
* (NodeValueTable::count()). */
|
||||
OAKENGINE_API int oakengine_traverse_db_row_count(
|
||||
const OakEngineTraverseDb *db, int input_index);
|
||||
|
||||
/**
|
||||
* @brief Row accessors. `row` is 0-based in table order (the views
|
||||
* reverse it themselves where needed). Strings are valid until db is
|
||||
* freed.
|
||||
*
|
||||
* - type: oak_node_value_type of the value.
|
||||
* - source: borrowed node that produced the value, or NULL.
|
||||
* - tag: the value's tag (may be empty, never NULL).
|
||||
* - value_string: NodeValue::value_to_string(value, false).
|
||||
* - split_count / split_string: NodeValue::to_split_value() count and
|
||||
* NodeValue::value_to_string(type, split[k], true) per element.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_traverse_row_type(const OakEngineTraverseDb *db,
|
||||
int input_index, int row);
|
||||
OAKENGINE_API OakEngineNode *oakengine_traverse_row_source(
|
||||
const OakEngineTraverseDb *db, int input_index, int row);
|
||||
OAKENGINE_API const char *oakengine_traverse_row_tag(
|
||||
const OakEngineTraverseDb *db, int input_index, int row);
|
||||
OAKENGINE_API const char *oakengine_traverse_row_value_string(
|
||||
const OakEngineTraverseDb *db, int input_index, int row);
|
||||
OAKENGINE_API int oakengine_traverse_row_split_count(
|
||||
const OakEngineTraverseDb *db, int input_index, int row);
|
||||
OAKENGINE_API const char *oakengine_traverse_row_split_string(
|
||||
const OakEngineTraverseDb *db, int input_index, int row, int split);
|
||||
|
||||
/**
|
||||
* @brief The table element selected by `hint_node`'s value hint for input
|
||||
* `input_id`@`element` against a table produced by
|
||||
* oakengine_traverse_generate_table() (pass its db; must contain exactly
|
||||
* one entry) -- NodeTraverser::generate_row_value_element_index().
|
||||
* Returns -1 when no element matches.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_traverse_table_element_index_for_hint(
|
||||
OakEngineNode *hint_node, const char *input_id, int element,
|
||||
const OakEngineTraverseDb *table_db);
|
||||
|
||||
/**
|
||||
* @brief Fill a caller-allocated olive::NodeValueRow with `node`'s input
|
||||
* values over the given range (NodeTraverser::generate_row()) -- the
|
||||
* viewer display gizmo drag-start path. `cache_video_params` (may be NULL
|
||||
* for engine defaults) and `sample_rate`/`channel_layout` seed the
|
||||
* traverser's cache params (NodeTraverser::set_cache_video_params /
|
||||
* set_cache_audio_params).
|
||||
*
|
||||
* Transition bridge (same pattern as
|
||||
* replaced by internal ColorTransformJob API): `row_out` is opaque to C
|
||||
* consumers; the application passes a pointer to its own
|
||||
* olive::NodeValueRow (a QHash typedef, no engine symbols) which the
|
||||
* engine fills in place.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_traverse_generate_row(
|
||||
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
|
||||
int64_t out_den, const oak_video_params *cache_video_params,
|
||||
int sample_rate, uint64_t channel_layout, void *row_out);
|
||||
|
||||
/**
|
||||
* @brief Accumulate the transform from `start` to `end` over the given
|
||||
* range (NodeTraverser::transform()) and return it as the 6 affine
|
||||
* coefficients of a QTransform (m11, m12, m21, m22, dx, dy), suitable for
|
||||
* `QTransform(m11, m12, m21, m22, dx, dy)`. `cache_video_params` may be
|
||||
* NULL for engine defaults.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_traverse_transform(
|
||||
OakEngineNode *start, OakEngineNode *end, int64_t in_num, int64_t in_den,
|
||||
int64_t out_num, int64_t out_den, const oak_video_params *cache_video_params,
|
||||
double out_m[6]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_TRAVERSE_H */
|
||||
@@ -0,0 +1,272 @@
|
||||
/***
|
||||
|
||||
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_UNDO_H
|
||||
#define OAKENGINE_UNDO_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "init.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file undo.h
|
||||
* @brief C ABI for the global undo stack (olive::UndoStack)
|
||||
*
|
||||
* Exposes the process-wide undo stack that backs every editing primitive:
|
||||
* pushing commands (the command objects themselves are still created by
|
||||
* the caller as opaque engine pointers), jumping to an arbitrary history
|
||||
* position, reading the command list for a history view, and the
|
||||
* undo/redo QActions for menus.
|
||||
*
|
||||
* Change notification: subscribe to OAKENGINE_EVENT_UNDO_INDEX_CHANGED on
|
||||
* oakengine_undo_handle(). The event fires after every stack mutation
|
||||
* (push/undo/redo/jump/clear); the command list must be re-read through
|
||||
* oakengine_undo_count()/oakengine_undo_command_text().
|
||||
*
|
||||
* Conventions (matching oakengine/project.h):
|
||||
* - Return codes: 0 (OAKENGINE_OK) on success, negative OAKENGINE_E_*
|
||||
* on failure. Functions returning a value return OAKENGINE_E_INVALID
|
||||
* when no application core exists.
|
||||
* - String output uses the buf/size convention.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Borrowed handle of the global undo stack, for use as the
|
||||
* subscription handle of OAKENGINE_EVENT_UNDO_INDEX_CHANGED. Returns NULL
|
||||
* when no application core exists.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_undo_handle(void);
|
||||
|
||||
/**
|
||||
* @brief Push `command` (an opaque `olive::UndoCommand *`, e.g. the result
|
||||
* of oakengine_task_import_get_command()) onto the stack and execute its
|
||||
* redo. Takes ownership of `command` (an empty MultiUndoCommand is deleted
|
||||
* immediately, matching UndoStack::push). `name` is the user-visible
|
||||
* command label (NULL behaves like an empty label).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_push(void *command, const char *name);
|
||||
|
||||
/**
|
||||
* @brief Start collecting: subsequent facade undoable operations are added
|
||||
* as children to a group and executed eagerly, but not pushed individually.
|
||||
* Returns OAKENGINE_E_STATE if a group is already open.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_group_begin(const char *name);
|
||||
|
||||
/**
|
||||
* @brief End the group and push it as ONE undo entry.
|
||||
*
|
||||
* An empty group is discarded (no undo entry). Returns OAKENGINE_E_STATE
|
||||
* if no group is open.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_group_end(void);
|
||||
|
||||
/**
|
||||
* @brief Abort the open group: undo all already-executed children and
|
||||
* discard the group. Returns OAKENGINE_E_STATE if no group is open.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_group_abort(void);
|
||||
|
||||
/**
|
||||
* @brief Execute the redo of `command` without taking ownership
|
||||
* (UndoCommand::redo_now semantics). This is the facade replacement for
|
||||
* app code that used to call MultiUndoCommand::redo_now() directly.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_command_redo_now(void *command);
|
||||
|
||||
/**
|
||||
* @brief Execute the undo of `command` without taking ownership
|
||||
* (UndoCommand::undo_now semantics).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_command_undo_now(void *command);
|
||||
|
||||
/**
|
||||
* @brief Callback signatures for app-defined undo commands.
|
||||
*
|
||||
* These allow UI-side code to create undoable actions without defining
|
||||
* C++ subclasses of olive::UndoCommand. The engine wraps the callbacks
|
||||
* in an internal UndoCommand and forwards redo/undo/free calls.
|
||||
*/
|
||||
typedef void (*oakengine_undo_command_redo_fn)(void *userdata);
|
||||
typedef void (*oakengine_undo_command_undo_fn)(void *userdata);
|
||||
typedef void (*oakengine_undo_command_free_fn)(void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Create an app-defined undo command backed by C callbacks.
|
||||
*
|
||||
* The returned pointer is an opaque `olive::UndoCommand *` suitable for
|
||||
* oakengine_undo_push() or oakengine_undo_command_multi_add_child().
|
||||
* The command takes ownership of `userdata`; `free_fn` is called when
|
||||
* the command is destroyed (whether pushed or freed directly).
|
||||
*
|
||||
* `name` is the user-visible label. Any callback may be NULL; a NULL
|
||||
* redo/undo callback makes that direction a no-op.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_undo_command_create(
|
||||
const char *name,
|
||||
oakengine_undo_command_redo_fn redo,
|
||||
oakengine_undo_command_undo_fn undo,
|
||||
oakengine_undo_command_free_fn free_fn,
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Create an empty MultiUndoCommand as an opaque command pointer.
|
||||
*
|
||||
* The returned pointer is owned by the caller until it is passed to
|
||||
* oakengine_undo_push() or freed with oakengine_undo_command_free().
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_undo_command_create_multi(void);
|
||||
|
||||
OAKENGINE_API void *oakengine_node_add_command(void *project, void *node);
|
||||
OAKENGINE_API void *oakengine_node_set_position_command(
|
||||
void *node, void *context, double x, double y, int expanded);
|
||||
OAKENGINE_API void *oakengine_node_remove_position_command(
|
||||
void *node, void *context);
|
||||
OAKENGINE_API void *oakengine_node_set_value_hint_command(
|
||||
void *node, const char *input, int element, int type, int index,
|
||||
const char *tag);
|
||||
OAKENGINE_API void *oakengine_node_remove_and_disconnect_command(void *node);
|
||||
|
||||
OAKENGINE_API void *oakengine_track_place_block_command(
|
||||
void *track_list, int track_index, void *block, int64_t in_ts);
|
||||
OAKENGINE_API void *oakengine_track_replace_block_with_gap_command(
|
||||
void *track, void *block, int handle_transitions);
|
||||
OAKENGINE_API void *oakengine_block_trim_command(
|
||||
void *track, void *block, int64_t new_length_num, int64_t new_length_den,
|
||||
int movement_mode, int roll_edit);
|
||||
OAKENGINE_API void *oakengine_transition_remove_command(
|
||||
void *transition, int remove_from_graph);
|
||||
OAKENGINE_API void *oakengine_track_slide_command(
|
||||
void *track, void *const *blocks, int block_count,
|
||||
void *in_adjacent, void *out_adjacent,
|
||||
int64_t movement_num, int64_t movement_den);
|
||||
OAKENGINE_API void *oakengine_block_split_preserving_links_command(
|
||||
void *const *blocks, int count, int64_t point_ts);
|
||||
OAKENGINE_API void *oakengine_block_split_get_split(
|
||||
void *command, void *block, int time_index);
|
||||
OAKENGINE_API void *oakengine_block_resize_with_media_in_command(
|
||||
void *block, int64_t length_num, int64_t length_den);
|
||||
OAKENGINE_API void *oakengine_block_set_media_in_command(
|
||||
void *block, int64_t media_in_num, int64_t media_in_den);
|
||||
OAKENGINE_API void *oakengine_timeline_ripple_delete_gaps_command(
|
||||
void *sequence, const int64_t *range_in_ts, const int64_t *range_out_ts,
|
||||
const int *track_types, const int *track_indexes, int range_count);
|
||||
|
||||
/**
|
||||
* @brief Create a TrackListInsertGaps command as an opaque command pointer.
|
||||
* `point_num`/`point_den` is the insertion point in rational seconds;
|
||||
* `length_num`/`length_den` is the gap length in rational seconds.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_track_list_insert_gaps_command(
|
||||
void *track_list, int64_t point_num, int64_t point_den,
|
||||
int64_t length_num, int64_t length_den);
|
||||
|
||||
/**
|
||||
* @brief Add `child` (an opaque command pointer) to the MultiUndoCommand
|
||||
* `multi`. Returns OAKENGINE_OK on success, OAKENGINE_E_INVALID if either
|
||||
* argument is NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_command_multi_add_child(void *multi,
|
||||
void *child);
|
||||
|
||||
/**
|
||||
* @brief Return the number of children in the MultiUndoCommand `multi`,
|
||||
* or OAKENGINE_E_INVALID if `multi` is NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_command_multi_child_count(void *multi);
|
||||
|
||||
/**
|
||||
* @brief Destroy a command created by oakengine_undo_command_create() or
|
||||
* oakengine_undo_command_create_multi() without pushing it onto the stack.
|
||||
* Commands passed to oakengine_undo_push() are owned by the stack and
|
||||
* must not be freed by the caller.
|
||||
*/
|
||||
OAKENGINE_API void oakengine_undo_command_free(void *command);
|
||||
|
||||
/**
|
||||
* @brief Total number of history rows (done + undone commands), or
|
||||
* OAKENGINE_E_INVALID when no stack exists.
|
||||
*/
|
||||
OAKENGINE_API int64_t oakengine_undo_count(void);
|
||||
|
||||
/**
|
||||
* @brief Current position in the history: the number of done commands
|
||||
* (rows below this index are undone). Emitted as payload `a` of
|
||||
* OAKENGINE_EVENT_UNDO_INDEX_CHANGED.
|
||||
*/
|
||||
OAKENGINE_API int64_t oakengine_undo_index(void);
|
||||
|
||||
/**
|
||||
* @brief Label of the history row at `row` (0-based, buf/size convention).
|
||||
* Falls back to the translated "Command" placeholder for empty labels.
|
||||
*
|
||||
* @return the label length, or OAKENGINE_E_NOT_FOUND for an invalid row.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_command_text(int64_t row, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief 1 when the row at `row` is currently done (not undone), 0 when it
|
||||
* is undone, OAKENGINE_E_NOT_FOUND for an invalid row.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_command_is_done(int64_t row);
|
||||
|
||||
/**
|
||||
* @brief Undo/redo until the done-command count equals `index`
|
||||
* (UndoStack::jump semantics).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_jump(int64_t index);
|
||||
|
||||
/**
|
||||
* @brief Delete all commands and push the fresh "New/Open Project" empty
|
||||
* command (UndoStack::clear).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_clear(void);
|
||||
|
||||
/**
|
||||
* @brief Refresh the undo/redo action labels and enabled state
|
||||
* (UndoStack::update_actions).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_update_actions(void);
|
||||
|
||||
/**
|
||||
* @brief 1/0 whether undo (redo) is currently possible,
|
||||
* OAKENGINE_E_INVALID when no stack exists.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_undo_can_undo(void);
|
||||
OAKENGINE_API int oakengine_undo_can_redo(void);
|
||||
|
||||
/**
|
||||
* @brief The stack's undo (redo) QAction as an opaque `void *` (actually a
|
||||
* `QAction *`; Qt types are allowed at this boundary). Borrowed; owned by
|
||||
* the stack. NULL when no stack exists.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_undo_undo_action(void);
|
||||
OAKENGINE_API void *oakengine_undo_redo_action(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_UNDO_H */
|
||||
@@ -0,0 +1,211 @@
|
||||
/***
|
||||
|
||||
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_VIDEOPARAMS_H
|
||||
#define OAKENGINE_VIDEOPARAMS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file videoparams.h
|
||||
* @brief C ABI POD and static-data accessors for the engine's VideoParams
|
||||
*
|
||||
* Covers the parameter surface the export and sequence dialogs need:
|
||||
* the POD carried by the encoding family (oakengine/encoding.h) and the
|
||||
* static metadata behind the standard combo boxes (supported frame rates,
|
||||
* pixel aspect ratios, dividers, pixel format names). Display/render-path
|
||||
* helpers (bytes per pixel, scaled texture sizes, ...) are out of scope for
|
||||
* now.
|
||||
*
|
||||
* Conventions match the other facade families: buf/size strings (return
|
||||
* value is the would-be length excluding the NUL), -1 on invalid indexes,
|
||||
* 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes where applicable.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief POD mirror of olive::VideoParams' user-facing fields.
|
||||
*
|
||||
* `time_base_*` is the frame duration (frame rate flipped), matching
|
||||
* VideoParams::time_base(); the frame rate is den/num. `format` is an
|
||||
* olive::PixelFormat::Format value, `interlacing` an
|
||||
* olive::VideoParams::Interlacing value (0 = none/progressive, 1 = top
|
||||
* field first, 2 = bottom field first), `color_range` an
|
||||
* olive::VideoParams::ColorRange value. The video channel count is an
|
||||
* engine-internal constant and not exposed.
|
||||
*/
|
||||
typedef struct oak_video_params {
|
||||
int width;
|
||||
int height;
|
||||
int time_base_num; /**< Frame duration numerator (e.g. 1001/30000 s). */
|
||||
int time_base_den;
|
||||
int format; /**< olive::PixelFormat::Format. */
|
||||
int pixel_aspect_num;
|
||||
int pixel_aspect_den;
|
||||
int interlacing; /**< olive::VideoParams::Interlacing. */
|
||||
int color_range; /**< olive::VideoParams::ColorRange. */
|
||||
int divider; /**< Preview resolution divider (1 = full). */
|
||||
/* The two fields below are only populated by the viewer family
|
||||
* (oakengine_viewer_get_video_params(), B8c); other producers leave
|
||||
* them 0 (k_video_type_video / not premultiplied). */
|
||||
int video_type; /**< olive::VideoParams::Type. */
|
||||
int premultiplied_alpha; /**< 0/1. */
|
||||
} oak_video_params;
|
||||
|
||||
/** @brief Number of standard frame rates (VideoParams::k_supported_frame_rates). */
|
||||
OAKENGINE_API int oakengine_video_params_supported_frame_rate_count(void);
|
||||
|
||||
/**
|
||||
* @brief The `index`-th standard frame rate as num/den (e.g. 24000/1001);
|
||||
* OAKENGINE_E_INVALID when out of range.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_supported_frame_rate_at(int index,
|
||||
int *num,
|
||||
int *den);
|
||||
|
||||
/**
|
||||
* @brief User-friendly label of a frame rate num/den pair
|
||||
* (VideoParams::frame_rate_to_string(); buf/size).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_frame_rate_to_string(int num, int den,
|
||||
char *buf,
|
||||
int buf_size);
|
||||
|
||||
/** @brief Number of standard pixel aspect ratios. */
|
||||
OAKENGINE_API int oakengine_video_params_standard_pixel_aspect_count(void);
|
||||
|
||||
/** @brief The `index`-th standard pixel aspect ratio as num/den. */
|
||||
OAKENGINE_API int oakengine_video_params_standard_pixel_aspect_at(int index,
|
||||
int *num,
|
||||
int *den);
|
||||
|
||||
/** @brief Display name of the `index`-th standard pixel aspect (buf/size). */
|
||||
OAKENGINE_API int
|
||||
oakengine_video_params_standard_pixel_aspect_name(int index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief VideoParams::format_pixel_aspect_ratio_string(): formats `format`
|
||||
* (a printf-style "%1" template) with the pixel aspect ratio num/den
|
||||
* (buf/size).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_format_pixel_aspect_ratio_string(
|
||||
const char *format, int num, int den, char *buf, int buf_size);
|
||||
|
||||
/** @brief Number of supported preview dividers. */
|
||||
OAKENGINE_API int oakengine_video_params_supported_divider_count(void);
|
||||
|
||||
/** @brief The `index`-th supported divider; -1 when out of range. */
|
||||
OAKENGINE_API int oakengine_video_params_supported_divider_at(int index);
|
||||
|
||||
/** @brief Display name of a divider (VideoParams::get_name_for_divider()). */
|
||||
OAKENGINE_API int oakengine_video_params_divider_name(int divider, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief 1 when `format` (a PixelFormat::Format value) is a float format
|
||||
* (VideoParams::format_is_float()).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_format_is_float(int format);
|
||||
|
||||
/** @brief Display name of a PixelFormat::Format value (buf/size). */
|
||||
OAKENGINE_API int oakengine_video_params_pixel_format_name(int format,
|
||||
char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Effective (divider-scaled) dimensions of width/height at `divider`
|
||||
* (VideoParams::effective_width()/effective_height()). Any output pointer
|
||||
* may be NULL.
|
||||
*
|
||||
* @return OAKENGINE_OK, or OAKENGINE_E_INVALID for non-positive
|
||||
* width/height/divider.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_effective_size(int width, int height,
|
||||
int divider,
|
||||
int *out_width,
|
||||
int *out_height);
|
||||
|
||||
/**
|
||||
* @brief Fill an oak_video_params POD (the display-path VideoParams
|
||||
* constructor equivalent). No validation is performed beyond rejecting a
|
||||
* NULL `p`; use oakengine_video_params_is_valid() to validate.
|
||||
*
|
||||
* @return OAKENGINE_OK, or OAKENGINE_E_INVALID for NULL `p`.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_make(oak_video_params *p, int width,
|
||||
int height, int time_base_num,
|
||||
int time_base_den, int format,
|
||||
int pixel_aspect_num,
|
||||
int pixel_aspect_den,
|
||||
int interlacing, int color_range,
|
||||
int divider);
|
||||
|
||||
/**
|
||||
* @brief Create an engine-side olive::VideoParams object from a POD.
|
||||
*
|
||||
* The returned pointer must be freed with oakengine_video_params_free().
|
||||
* This is the only legal way for app code to construct a VideoParams object
|
||||
* during the R6 C ABI migration.
|
||||
*
|
||||
* @return Engine-owned VideoParams pointer, or NULL if pod is NULL.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_video_params_create(const oak_video_params *pod);
|
||||
|
||||
/** @brief Free a VideoParams object created by oakengine_video_params_create(). */
|
||||
OAKENGINE_API void oakengine_video_params_free(void *params);
|
||||
|
||||
/**
|
||||
* @brief 1 when all user-facing fields of `a` and `b` match
|
||||
* (VideoParams::operator==), 0 otherwise or when either is NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_equal(const oak_video_params *a,
|
||||
const oak_video_params *b);
|
||||
|
||||
/**
|
||||
* @brief 1 when the POD describes a usable video stream
|
||||
* (VideoParams::is_valid(): positive dimensions, non-null pixel aspect,
|
||||
* in-range pixel format), 0 otherwise or when `p` is NULL.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_is_valid(const oak_video_params *p);
|
||||
|
||||
/**
|
||||
* @brief Bytes per pixel of `format` (a PixelFormat::Format value) with
|
||||
* `channels` channels (VideoParams::get_bytes_per_pixel()).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_bytes_per_pixel(int format,
|
||||
int channels);
|
||||
|
||||
/**
|
||||
* @brief The engine-internal video channel count
|
||||
* (VideoParams::k_internal_channel_count, i.e. RGBA).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_video_params_internal_channel_count(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_VIDEOPARAMS_H */
|
||||
@@ -0,0 +1,350 @@
|
||||
/***
|
||||
|
||||
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_VIEWER_H
|
||||
#define OAKENGINE_VIEWER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "node.h"
|
||||
#include "timeline.h"
|
||||
#include "videoparams.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file viewer.h
|
||||
* @brief C ABI for viewer nodes (olive::ViewerOutput and subclasses:
|
||||
* Sequence, Footage)
|
||||
*
|
||||
* A viewer node is the bridge between a node graph and a monitor: it owns
|
||||
* a playhead, a length, per-stream video/audio/subtitle parameters, a
|
||||
* workarea and a marker list. This family covers the application-side
|
||||
* uses of olive::ViewerOutput that are not already exposed through the
|
||||
* sequence (timeline.h) or node (node.h) families.
|
||||
*
|
||||
* Handles: a viewer handle is simply an OakEngineNode* whose engine object
|
||||
* is a ViewerOutput (validate with oakengine_viewer_from_node()). Borrowed,
|
||||
* same lifetime rules as node.h. Change notifications (length/playhead/
|
||||
* params/workarea-adjacent) are delivered through the event mechanism --
|
||||
* subscribe with the OAKENGINE_EVENT_VIEWER_* ids from oakengine/events.h
|
||||
* on the node handle.
|
||||
*
|
||||
* Conventions match the rest of the facade: rationals are int64
|
||||
* numerator/denominator pairs (seconds), booleans are int, 0
|
||||
* (OAKENGINE_OK)/negative OAKENGINE_E_* return codes, NULL handles are
|
||||
* no-ops returning OAKENGINE_E_INVALID.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief POD snapshot of a viewer's workarea (olive::TimelineWorkArea:
|
||||
* range in/out + enabled flag). Rationals in seconds.
|
||||
*/
|
||||
typedef struct oakengine_viewer_workarea {
|
||||
int64_t in_num;
|
||||
int64_t in_den;
|
||||
int64_t out_num;
|
||||
int64_t out_den;
|
||||
int enabled;
|
||||
} oakengine_viewer_workarea;
|
||||
|
||||
/**
|
||||
* @brief Return `node` if its engine object is a viewer (olive::ViewerOutput
|
||||
* or subclass, e.g. Sequence/Footage), NULL otherwise. Replaces
|
||||
* dynamic_cast<ViewerOutput*> at the app boundary; also the canonical way
|
||||
* to validate a handle for this family.
|
||||
*/
|
||||
OAKENGINE_API OakEngineNode *oakengine_viewer_from_node(OakEngineNode *node);
|
||||
|
||||
/** @brief const overload of oakengine_viewer_from_node(). */
|
||||
OAKENGINE_API const OakEngineNode *
|
||||
oakengine_viewer_from_const_node(const OakEngineNode *node);
|
||||
|
||||
/* ---- Input ids / constants (ViewerOutput::k_* statics) ------------------ */
|
||||
|
||||
/** @brief ViewerOutput::k_video_params_input. Static string, never freed. */
|
||||
OAKENGINE_API const char *oakengine_viewer_video_params_input_id(void);
|
||||
/** @brief ViewerOutput::k_audio_params_input. */
|
||||
OAKENGINE_API const char *oakengine_viewer_audio_params_input_id(void);
|
||||
/** @brief ViewerOutput::k_subtitle_params_input. */
|
||||
OAKENGINE_API const char *oakengine_viewer_subtitle_params_input_id(void);
|
||||
/** @brief ViewerOutput::k_texture_input. */
|
||||
OAKENGINE_API const char *oakengine_viewer_texture_input_id(void);
|
||||
/** @brief ViewerOutput::k_samples_input. */
|
||||
OAKENGINE_API const char *oakengine_viewer_samples_input_id(void);
|
||||
/** @brief ViewerOutput::k_default_sample_format (olive::core::SampleFormat). */
|
||||
OAKENGINE_API int oakengine_viewer_default_sample_format(void);
|
||||
|
||||
/* ---- Playhead / length --------------------------------------------------- */
|
||||
|
||||
/** @brief Current playhead in seconds (ViewerOutput::get_playhead()). */
|
||||
OAKENGINE_API int oakengine_viewer_get_playhead(const OakEngineNode *self,
|
||||
int64_t *num, int64_t *den);
|
||||
|
||||
/** @brief Move the playhead (ViewerOutput::set_playhead()). Emits
|
||||
* OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED. */
|
||||
OAKENGINE_API int oakengine_viewer_set_playhead(OakEngineNode *self,
|
||||
int64_t num, int64_t den);
|
||||
|
||||
/**
|
||||
* @brief Set the video parameters of stream `index` on `self`
|
||||
* (ViewerOutput::set_video_params()). `self` must be a viewer node.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_set_video_params(OakEngineNode *self,
|
||||
const oak_video_params *params,
|
||||
int index);
|
||||
|
||||
/**
|
||||
* @brief Set the audio parameters of stream `index` on `self`
|
||||
* (ViewerOutput::set_audio_params()). `self` must be a viewer node.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_set_audio_params(OakEngineNode *self,
|
||||
int sample_rate,
|
||||
uint64_t channel_layout,
|
||||
int format, int index);
|
||||
|
||||
/** @brief Content length in seconds (ViewerOutput::get_length()). */
|
||||
OAKENGINE_API int oakengine_viewer_get_length(const OakEngineNode *self,
|
||||
int64_t *num, int64_t *den);
|
||||
|
||||
/** @brief Video content length in seconds (ViewerOutput::get_video_length()). */
|
||||
OAKENGINE_API int oakengine_viewer_get_video_length(const OakEngineNode *self,
|
||||
int64_t *num,
|
||||
int64_t *den);
|
||||
|
||||
/** @brief Audio content length in seconds (ViewerOutput::get_audio_length()). */
|
||||
OAKENGINE_API int oakengine_viewer_get_audio_length(const OakEngineNode *self,
|
||||
int64_t *num,
|
||||
int64_t *den);
|
||||
|
||||
/* ---- Stream parameters ---------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Video params of stream `index` (ViewerOutput::get_video_params()).
|
||||
* `out` is always written; an out-of-range index yields a zeroed struct
|
||||
* (width/height 0 = invalid, matches an invalid olive::VideoParams).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_get_video_params(
|
||||
const OakEngineNode *self, int index, oak_video_params *out);
|
||||
|
||||
/**
|
||||
* @brief Audio params of stream `index` (ViewerOutput::get_audio_params()).
|
||||
* Any of the out pointers may be NULL. `format` is an
|
||||
* olive::core::SampleFormat value; out-of-range index yields 0/0/0.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_get_audio_params(
|
||||
const OakEngineNode *self, int index, int *sample_rate,
|
||||
uint64_t *channel_layout, int *format);
|
||||
|
||||
/** @brief Number of video streams (ViewerOutput::get_video_stream_count()). */
|
||||
OAKENGINE_API int oakengine_viewer_get_video_stream_count(
|
||||
const OakEngineNode *self);
|
||||
/** @brief Number of audio streams (ViewerOutput::get_audio_stream_count()). */
|
||||
OAKENGINE_API int oakengine_viewer_get_audio_stream_count(
|
||||
const OakEngineNode *self);
|
||||
/** @brief Number of subtitle streams (ViewerOutput::get_subtitle_stream_count()). */
|
||||
OAKENGINE_API int oakengine_viewer_get_subtitle_stream_count(
|
||||
const OakEngineNode *self);
|
||||
|
||||
/**
|
||||
* @brief 1 if stream `index` of `track_type` (OAKENGINE_TRACK_TYPE_*) is
|
||||
* enabled (VideoParams/AudioParams/SubtitleParams::enabled()), else 0;
|
||||
* OAKENGINE_E_INVALID (< 0) on bad arguments.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_get_stream_enabled(
|
||||
const OakEngineNode *self, int track_type, int index);
|
||||
|
||||
/**
|
||||
* @brief Number of subtitles in subtitle stream `index`
|
||||
* (SubtitleParams::size()); < 0 on bad arguments.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_get_subtitle_count(
|
||||
const OakEngineNode *self, int index);
|
||||
|
||||
/**
|
||||
* @brief Borrowed pointer to subtitle `sub_index` of subtitle stream
|
||||
* `index` (a const olive::Subtitle*; the application copies the value out,
|
||||
* it must not free or store it beyond the footage's lifetime). NULL on
|
||||
* bad arguments.
|
||||
*/
|
||||
OAKENGINE_API const void *oakengine_viewer_get_subtitle_at(
|
||||
const OakEngineNode *self, int index, int sub_index);
|
||||
|
||||
/**
|
||||
* @brief 1 if the viewer has at least one enabled stream of `track_type`
|
||||
* (OAKENGINE_TRACK_TYPE_* from timeline.h), else 0
|
||||
* (ViewerOutput::has_enabled_video/audio/subtitle_streams()).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_has_enabled_streams(
|
||||
const OakEngineNode *self, int track_type);
|
||||
|
||||
/**
|
||||
* @brief Params of the first enabled video stream
|
||||
* (ViewerOutput::get_first_enabled_video_stream()); zeroed struct when
|
||||
* none is enabled.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_get_first_enabled_video_stream(
|
||||
const OakEngineNode *self, oak_video_params *out);
|
||||
|
||||
/**
|
||||
* @brief Number of enabled streams of all types
|
||||
* (ViewerOutput::get_enabled_streams_as_references().size()).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_get_enabled_stream_count(
|
||||
const OakEngineNode *self);
|
||||
|
||||
/**
|
||||
* @brief Write the enabled stream references
|
||||
* (ViewerOutput::get_enabled_streams_as_references()) into caller arrays:
|
||||
* `types[k]` = OAKENGINE_TRACK_TYPE_*, `indices[k]` = stream index within
|
||||
* that type. At most `max` entries are written; returns the total count
|
||||
* (call with max=0/NULL arrays to query, or use
|
||||
* oakengine_viewer_get_enabled_stream_count()).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_get_enabled_streams(
|
||||
const OakEngineNode *self, int *types, int *indices, int max);
|
||||
|
||||
/* ---- Workarea -------------------------------------------------------------- */
|
||||
|
||||
/** @brief Snapshot of the viewer's workarea (ViewerOutput::get_work_area()
|
||||
* range/enabled as POD). */
|
||||
OAKENGINE_API int oakengine_viewer_get_workarea(
|
||||
const OakEngineNode *self, oakengine_viewer_workarea *out);
|
||||
|
||||
/** @brief Set the workarea range (TimelineWorkArea::set_range()). Emits the
|
||||
* workarea range notification on the underlying workarea object. */
|
||||
OAKENGINE_API int oakengine_viewer_set_workarea_range(OakEngineNode *self,
|
||||
int64_t in_num,
|
||||
int64_t in_den,
|
||||
int64_t out_num,
|
||||
int64_t out_den);
|
||||
|
||||
/** @brief Enable/disable the workarea (TimelineWorkArea::set_enabled()). */
|
||||
OAKENGINE_API int oakengine_viewer_set_workarea_enabled(OakEngineNode *self,
|
||||
int enabled);
|
||||
|
||||
/* ---- Parameter setup / waveform --------------------------------------------- */
|
||||
|
||||
/** @brief Apply the application default parameters
|
||||
* (ViewerOutput::set_default_parameters(): width/height/pixel aspect/
|
||||
* interlacing/audio layout from Config, frame rate from
|
||||
* DefaultSequenceFrameRate). */
|
||||
OAKENGINE_API int oakengine_viewer_set_default_parameters(OakEngineNode *self);
|
||||
|
||||
/**
|
||||
* @brief Create a command that sets the viewer's preview resolution divider
|
||||
* (changes the k_video_params_input standard value). Returns an opaque command
|
||||
* pointer, or NULL when `self` is not a viewer or `divider` is invalid.
|
||||
*/
|
||||
OAKENGINE_API void *oakengine_viewer_set_preview_divider_command(
|
||||
OakEngineNode *self, int divider);
|
||||
|
||||
/**
|
||||
* @brief Adopt the parameters of the given footage viewers
|
||||
* (ViewerOutput::set_parameters_from_footage()). Every element of
|
||||
* `footage` must itself be a viewer handle.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_viewer_set_parameters_from_footage(
|
||||
OakEngineNode *self, OakEngineNode *const *footage, int count);
|
||||
|
||||
/** @brief Enable/disable waveform cache requests
|
||||
* (ViewerOutput::set_waveform_enabled()). */
|
||||
OAKENGINE_API int oakengine_viewer_set_waveform_enabled(OakEngineNode *self,
|
||||
int enabled);
|
||||
|
||||
/**
|
||||
* @brief The waveform cache of the connected sample output, or NULL
|
||||
* (ViewerOutput::get_connected_waveform()). Opaque borrowed pointer; the
|
||||
* application only passes it through to its own audio monitor, it must not
|
||||
* dereference it.
|
||||
*/
|
||||
OAKENGINE_API const void *
|
||||
oakengine_viewer_get_connected_waveform(const OakEngineNode *self);
|
||||
|
||||
/**
|
||||
* @brief Borrowed handle of the viewer's timeline marker list
|
||||
* (ViewerOutput::get_markers()), for the oakengine_marker_list_* family
|
||||
* and the OAKENGINE_EVENT_MARKER_LIST_* events. NULL when `self` is not a
|
||||
* viewer.
|
||||
*/
|
||||
OAKENGINE_API OakEngineMarkerList *
|
||||
oakengine_viewer_get_marker_list(OakEngineNode *self);
|
||||
|
||||
/**
|
||||
* @brief Borrowed handle of the viewer's workarea
|
||||
* (ViewerOutput::get_work_area()), for the oakengine_workarea_* family and
|
||||
* the OAKENGINE_EVENT_WORKAREA_* events. NULL when `self` is not a viewer.
|
||||
*/
|
||||
OAKENGINE_API OakEngineWorkarea *
|
||||
oakengine_viewer_get_workarea_handle(OakEngineNode *self);
|
||||
|
||||
/* ---- Playback cache / frame cache ------------------------------------------ */
|
||||
|
||||
/**
|
||||
* @brief Opaque playback cache handle (olive::PlaybackCache).
|
||||
*/
|
||||
typedef struct OakEnginePlaybackCache OakEnginePlaybackCache;
|
||||
|
||||
/**
|
||||
* @brief Opaque frame cache handle (olive::FrameHashCache).
|
||||
*/
|
||||
typedef struct OakEngineFrameCache OakEngineFrameCache;
|
||||
|
||||
/**
|
||||
* @brief Borrowed playback cache of a viewer's connected output
|
||||
* (ViewerOutput::get_connected_video_cache() for video, or from the
|
||||
* ClipBlock::connected_video_cache()). Returns NULL when not available
|
||||
* or when `self` is not a viewer/clip node.
|
||||
*/
|
||||
OAKENGINE_API OakEnginePlaybackCache *
|
||||
oakengine_viewer_get_playback_cache(OakEngineNode *self);
|
||||
|
||||
/**
|
||||
* @brief Static indicator height for playback cache rendering
|
||||
* (PlaybackCache::get_cache_indicator_height()). > 0.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_playback_cache_indicator_height(void);
|
||||
|
||||
/**
|
||||
* @brief Fill `ranges` with the valid (cached) time ranges from the
|
||||
* playback cache. `ranges` is an array of (in_num,in_den,out_num,out_den)
|
||||
* int64_t quads; at most `max` ranges are written. Returns the number of
|
||||
* ranges written, or OAKENGINE_E_INVALID on NULL cache.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_playback_cache_valid_ranges(
|
||||
OakEnginePlaybackCache *cache, int64_t *ranges, int max);
|
||||
|
||||
/**
|
||||
* @brief Borrowed frame hash cache (FrameHashCache) of a viewer node
|
||||
* (ViewerOutput has a get_video_cache(), etc.). Returns NULL when not
|
||||
* available or when `self` is not a viewer node.
|
||||
*/
|
||||
OAKENGINE_API OakEngineFrameCache *
|
||||
oakengine_viewer_get_frame_cache(OakEngineNode *self);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_VIEWER_H */
|
||||
@@ -0,0 +1,146 @@
|
||||
/***
|
||||
|
||||
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_WORKER_H
|
||||
#define OAKENGINE_WORKER_H
|
||||
|
||||
#include "export.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file worker.h
|
||||
* @brief C ABI for the render worker process logic
|
||||
*
|
||||
* The render worker (oak-render-worker) is a headless render process spawned
|
||||
* by the editor through the render worker pool. All of its runtime logic —
|
||||
* Qt application setup, render backend initialization, the startup handshake
|
||||
* and the NDJSON control-message loop — lives inside liboakengine behind this
|
||||
* pure C interface, so the worker executable itself contains no engine C++
|
||||
* ABI usage.
|
||||
*
|
||||
* Two entry levels are exposed:
|
||||
*
|
||||
* - oakengine_worker_main(): a drop-in main() for the worker executable.
|
||||
* It creates the QGuiApplication, parses --backend, initializes the
|
||||
* renderer, sends the startup handshake and runs the stdin/stdout NDJSON
|
||||
* loop until a shutdown message or EOF.
|
||||
*
|
||||
* - The OakWorkerSession family: the same message-handling state machine
|
||||
* in a transport-agnostic form, so tests (and alternative transports)
|
||||
* can drive it line by line without spawning a process. Responses that
|
||||
* the worker would write to stdout are returned through the buf/size
|
||||
* convention instead.
|
||||
*
|
||||
* Conventions (mirrors ipc.h):
|
||||
* - Returned handles are owned by the caller and must be released with the
|
||||
* matching _free(). NULL is accepted by every function and yields a
|
||||
* no-op / zero result.
|
||||
* - 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.
|
||||
*/
|
||||
|
||||
typedef struct OakWorkerSession OakWorkerSession;
|
||||
|
||||
/**
|
||||
* @brief Create a worker session for the given render backend.
|
||||
*
|
||||
* `backend` names the render backend ("opengl", "vulkan"); the session tries
|
||||
* the dynamic backend first and falls back to the direct OpenGL renderer,
|
||||
* exactly like the worker main. NULL, "" or "none" skips renderer creation
|
||||
* entirely, producing a session that can parse and answer control messages
|
||||
* but cannot actually render (useful for exercising error paths in tests).
|
||||
*
|
||||
* A QGuiApplication must exist before creating a session with a real
|
||||
* backend. The returned handle is owned by the caller.
|
||||
*/
|
||||
OAKENGINE_API OakWorkerSession *
|
||||
oakengine_worker_session_create(const char *backend);
|
||||
|
||||
OAKENGINE_API void oakengine_worker_session_free(OakWorkerSession *self);
|
||||
|
||||
/**
|
||||
* @brief 1 if the session holds a successfully initialized render backend.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_worker_session_has_renderer(const OakWorkerSession *self);
|
||||
|
||||
/**
|
||||
* @brief Load the engine runtime services the session depends on (config,
|
||||
* node factory, color manager, frame/disk managers, project serializer).
|
||||
*
|
||||
* Idempotent in practice: the underlying services are process-wide
|
||||
* singletons. Returns 1 on success, 0 on failure (NULL session).
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_worker_session_initialize_runtime(OakWorkerSession *self);
|
||||
|
||||
/**
|
||||
* @brief Build the startup handshake the worker sends to its parent
|
||||
* (buf/size convention).
|
||||
*
|
||||
* Announces the protocol version and, when a renderer is present, the
|
||||
* negotiated GL version. Returns the required size, or -1 on failure.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_worker_session_startup_handshake(OakWorkerSession *self, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Handle one NDJSON control line and produce the response, if any.
|
||||
*
|
||||
* `line` is one complete JSON message (with or without the trailing
|
||||
* newline). The response — what the worker main loop would write to stdout —
|
||||
* is serialized into response_buf using the buf/size convention: the return
|
||||
* value is the number of characters that would have been written excluding
|
||||
* the NUL, so 0 means "no response" (e.g. a successful handshake or a
|
||||
* shutdown message) and a positive value queries/fills the response. A
|
||||
* malformed `line` yields an error response, not a failure.
|
||||
*
|
||||
* Returns -1 when the handler itself failed (the worker main treats this as
|
||||
* a fatal error for its exit code, though it keeps draining input).
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_worker_session_handle_json(OakWorkerSession *self, const char *line,
|
||||
char *response_buf, int response_buf_size);
|
||||
|
||||
/**
|
||||
* @brief 1 once a shutdown control message has been received.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_worker_session_shutdown_requested(const OakWorkerSession *self);
|
||||
|
||||
/**
|
||||
* @brief Full render-worker main(). `argc`/`argv` are passed through from
|
||||
* the executable's main; "--backend <name>" selects the render backend.
|
||||
*
|
||||
* Returns the process exit code (0 on clean shutdown).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_worker_main(int argc, char **argv);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_WORKER_H */
|
||||
@@ -23,22 +23,61 @@
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
include/oakengine/init.h
|
||||
include/oakengine/events.h
|
||||
include/oakengine/app.h
|
||||
include/oakengine/project.h
|
||||
include/oakengine/timeline.h
|
||||
include/oakengine/renderer.h
|
||||
include/oakengine/display.h
|
||||
include/oakengine/footage.h
|
||||
include/oakengine/exporter.h
|
||||
include/oakengine/encoding.h
|
||||
include/oakengine/videoparams.h
|
||||
include/oakengine/color.h
|
||||
include/oakengine/node.h
|
||||
include/oakengine/playback.h
|
||||
include/oakengine/preview.h
|
||||
include/oakengine/sync.h
|
||||
include/oakengine/worker.h
|
||||
include/oakengine/viewer.h
|
||||
include/oakengine/traverse.h
|
||||
include/oakengine/task.h
|
||||
include/oakengine/undo.h
|
||||
include/oakengine/config.h
|
||||
include/oakengine/audio.h
|
||||
include/oakengine/disk.h
|
||||
include/oakengine/proxy.h
|
||||
include/oakengine/lut.h
|
||||
include/oakengine/serializer.h
|
||||
include/oakengine/plugin.h
|
||||
include/oakengine/gizmo.h
|
||||
src/capi/init.cpp
|
||||
src/capi/events.cpp
|
||||
src/capi/app.cpp
|
||||
src/capi/project.cpp
|
||||
src/capi/timeline.cpp
|
||||
src/capi/renderer.cpp
|
||||
src/capi/display.cpp
|
||||
src/capi/footage.cpp
|
||||
src/capi/export.cpp
|
||||
src/capi/encoding.cpp
|
||||
src/capi/color.cpp
|
||||
src/capi/node.cpp
|
||||
src/capi/playback.cpp
|
||||
src/capi/preview.cpp
|
||||
src/capi/sync.cpp
|
||||
src/capi/worker.cpp
|
||||
src/capi/viewer.cpp
|
||||
src/capi/traverse.cpp
|
||||
src/capi/task.cpp
|
||||
src/capi/undo.cpp
|
||||
src/capi/config.cpp
|
||||
src/capi/audio.cpp
|
||||
src/capi/disk.cpp
|
||||
src/capi/proxy.cpp
|
||||
src/capi/lut.cpp
|
||||
src/capi/serializer.cpp
|
||||
src/capi/plugin.cpp
|
||||
src/capi/gizmo.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -0,0 +1,812 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/app.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QString>
|
||||
|
||||
#include "coreengine.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "node/project/serializer/serializedlayoutinfo.h"
|
||||
#include "task/task.h"
|
||||
#include "undo/undostack.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::Project *impl(OakEngineProject *h)
|
||||
{
|
||||
return reinterpret_cast<olive::Project *>(h);
|
||||
}
|
||||
|
||||
OakEngineProject *wrap(olive::Project *p)
|
||||
{
|
||||
return reinterpret_cast<OakEngineProject *>(p);
|
||||
}
|
||||
|
||||
OakEngineSequence *wrap_seq(olive::Sequence *s)
|
||||
{
|
||||
return reinterpret_cast<OakEngineSequence *>(s);
|
||||
}
|
||||
|
||||
// buf/size convention: returns the would-be length excluding the NUL.
|
||||
int string_to_buf(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf = s.toUtf8();
|
||||
if (buf && buf_size > 0) {
|
||||
snprintf(buf, size_t(buf_size), "%s", utf.constData());
|
||||
}
|
||||
return int(utf.size());
|
||||
}
|
||||
|
||||
// Registered callback set (all fields may be null).
|
||||
OakEngineAppCallbacks g_callbacks = {};
|
||||
|
||||
// Whether oakengine_app_start() has run (and oakengine_app_stop() has not).
|
||||
bool g_started = false;
|
||||
|
||||
// The EngineCore the notification signals are currently connected to.
|
||||
olive::EngineCore *g_connected_core = nullptr;
|
||||
|
||||
olive::EngineCore *app_core()
|
||||
{
|
||||
return olive::EngineCore::instance();
|
||||
}
|
||||
|
||||
// The EngineCore constructor's UndoStack member creates QActions, which need
|
||||
// QGuiApplication state (same reason as oakengine_init()).
|
||||
void ensure_qcoreapplication()
|
||||
{
|
||||
if (QCoreApplication::instance()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
|
||||
qputenv("QT_QPA_PLATFORM", "offscreen");
|
||||
}
|
||||
|
||||
static int argc = 1;
|
||||
static char app_name[] = "oakengine";
|
||||
static char *argv[] = { app_name, nullptr };
|
||||
new QGuiApplication(argc, argv);
|
||||
|
||||
QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org"));
|
||||
QCoreApplication::setApplicationName(QStringLiteral("Oak Video Editor"));
|
||||
}
|
||||
|
||||
// Forward engine signals to the registered C callbacks. Connected once per
|
||||
// EngineCore instance; dropped events are fine while no callback is set.
|
||||
void connect_notifications(olive::EngineCore *core)
|
||||
{
|
||||
if (!core || g_connected_core == core) {
|
||||
return;
|
||||
}
|
||||
g_connected_core = core;
|
||||
|
||||
QObject::connect(core, &olive::EngineCore::status_message_show, core,
|
||||
[](const QString &message, int timeout) {
|
||||
if (g_callbacks.status_message_show) {
|
||||
g_callbacks.status_message_show(
|
||||
message.toUtf8().constData(), timeout,
|
||||
g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::status_message_clear, core,
|
||||
[] {
|
||||
if (g_callbacks.status_message_clear) {
|
||||
g_callbacks.status_message_clear(
|
||||
g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::cache_full_warning_requested,
|
||||
core, [] {
|
||||
if (g_callbacks.cache_full_warning) {
|
||||
g_callbacks.cache_full_warning(
|
||||
g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::active_project_changed, core,
|
||||
[](olive::Project *p) {
|
||||
if (g_callbacks.active_project_changed) {
|
||||
g_callbacks.active_project_changed(
|
||||
wrap(p), g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::tool_changed, core,
|
||||
[](const olive::Tool::Item &tool) {
|
||||
if (g_callbacks.tool_changed) {
|
||||
g_callbacks.tool_changed(int(tool),
|
||||
g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::addable_object_changed, core,
|
||||
[](olive::Tool::AddableObject o) {
|
||||
if (g_callbacks.addable_object_changed) {
|
||||
g_callbacks.addable_object_changed(
|
||||
int(o), g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::snapping_changed, core,
|
||||
[](const bool &b) {
|
||||
if (g_callbacks.snapping_changed) {
|
||||
g_callbacks.snapping_changed(b ? 1 : 0,
|
||||
g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::timecode_display_changed, core,
|
||||
[](olive::core::Timecode::Display d) {
|
||||
if (g_callbacks.timecode_display_changed) {
|
||||
g_callbacks.timecode_display_changed(
|
||||
int(d), g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::open_recent_list_changed, core,
|
||||
[] {
|
||||
if (g_callbacks.open_recent_list_changed) {
|
||||
g_callbacks.open_recent_list_changed(
|
||||
g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
QObject::connect(core, &olive::EngineCore::color_picker_enabled, core,
|
||||
[](bool e) {
|
||||
if (g_callbacks.color_picker_enabled) {
|
||||
g_callbacks.color_picker_enabled(
|
||||
e ? 1 : 0, g_callbacks.userdata);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Translate the C handler callbacks into the std::function handlers
|
||||
// EngineCore calls when it needs user interaction.
|
||||
void install_handlers(olive::EngineCore *core)
|
||||
{
|
||||
if (g_callbacks.confirm_image_sequence) {
|
||||
core->set_confirm_image_sequence_handler([](const QString &filename) {
|
||||
return g_callbacks.confirm_image_sequence(
|
||||
filename.toUtf8().constData(),
|
||||
g_callbacks.userdata) != 0;
|
||||
});
|
||||
} else {
|
||||
core->set_confirm_image_sequence_handler(nullptr);
|
||||
}
|
||||
|
||||
if (g_callbacks.relink_footage) {
|
||||
core->set_relink_handler([](QVector<olive::Footage *> footage) {
|
||||
return g_callbacks.relink_footage(
|
||||
reinterpret_cast<OakEngineFootage **>(footage.data()),
|
||||
int(footage.size()), g_callbacks.userdata) != 0;
|
||||
});
|
||||
} else {
|
||||
core->set_relink_handler(nullptr);
|
||||
}
|
||||
|
||||
if (g_callbacks.save_project) {
|
||||
core->set_save_project_handler([](const QString &override_filename) {
|
||||
g_callbacks.save_project(override_filename.toUtf8().constData(),
|
||||
g_callbacks.userdata);
|
||||
});
|
||||
} else {
|
||||
core->set_save_project_handler(nullptr);
|
||||
}
|
||||
|
||||
if (g_callbacks.close_project) {
|
||||
core->set_close_project_handler([] {
|
||||
return g_callbacks.close_project(g_callbacks.userdata) != 0;
|
||||
});
|
||||
} else {
|
||||
core->set_close_project_handler(nullptr);
|
||||
}
|
||||
|
||||
if (g_callbacks.load_layout) {
|
||||
core->set_load_layout_handler(
|
||||
[](const olive::SerializedLayoutInfo &layout) {
|
||||
g_callbacks.load_layout(&layout, g_callbacks.userdata);
|
||||
});
|
||||
} else {
|
||||
core->set_load_layout_handler(nullptr);
|
||||
}
|
||||
|
||||
#ifdef USE_OTIO
|
||||
if (g_callbacks.otio_import) {
|
||||
core->set_otio_import_handler(
|
||||
[](const QList<olive::Sequence *> &sequences) {
|
||||
QVector<OakEngineSequence *> handles;
|
||||
handles.reserve(sequences.size());
|
||||
for (olive::Sequence *s : sequences) {
|
||||
handles.append(wrap_seq(s));
|
||||
}
|
||||
return g_callbacks.otio_import(handles.data(),
|
||||
int(handles.size()),
|
||||
g_callbacks.userdata) != 0;
|
||||
});
|
||||
} else {
|
||||
core->set_otio_import_handler(nullptr);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
int oakengine_app_create(const OakEngineAppParams *params)
|
||||
{
|
||||
if (app_core()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
ensure_qcoreapplication();
|
||||
|
||||
olive::EngineCore::CoreParams core_params;
|
||||
if (params) {
|
||||
switch (params->run_mode) {
|
||||
case OAKENGINE_APP_RUN_HEADLESS_EXPORT:
|
||||
core_params.set_run_mode(
|
||||
olive::EngineCore::CoreParams::k_headless_export);
|
||||
break;
|
||||
case OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE:
|
||||
core_params.set_run_mode(
|
||||
olive::EngineCore::CoreParams::k_headless_pre_cache);
|
||||
break;
|
||||
default:
|
||||
core_params.set_run_mode(
|
||||
olive::EngineCore::CoreParams::k_run_normal);
|
||||
break;
|
||||
}
|
||||
core_params.set_fullscreen(params->fullscreen != 0);
|
||||
if (params->startup_project) {
|
||||
core_params.set_startup_project(
|
||||
QString::fromUtf8(params->startup_project));
|
||||
}
|
||||
if (params->startup_language) {
|
||||
core_params.set_startup_language(
|
||||
QString::fromUtf8(params->startup_language));
|
||||
}
|
||||
if (params->crash_on_startup) {
|
||||
core_params.set_crash_on_startup(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Never deleted: backs the process-wide EngineCore singleton (same
|
||||
// lifetime rule as the oakengine_init() shell).
|
||||
new olive::EngineCore(core_params);
|
||||
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_start(void)
|
||||
{
|
||||
if (!app_core() || g_started) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
app_core()->start();
|
||||
g_started = true;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_stop(void)
|
||||
{
|
||||
if (!app_core() || !g_started) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
app_core()->stop();
|
||||
g_started = false;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_set_callbacks(const OakEngineAppCallbacks *callbacks)
|
||||
{
|
||||
if (callbacks) {
|
||||
g_callbacks = *callbacks;
|
||||
} else {
|
||||
g_callbacks = OakEngineAppCallbacks{};
|
||||
}
|
||||
|
||||
if (olive::EngineCore *core = app_core()) {
|
||||
connect_notifications(core);
|
||||
install_handlers(core);
|
||||
}
|
||||
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_run_mode(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
switch (app_core()->core_params().run_mode()) {
|
||||
case olive::EngineCore::CoreParams::k_headless_export:
|
||||
return OAKENGINE_APP_RUN_HEADLESS_EXPORT;
|
||||
case olive::EngineCore::CoreParams::k_headless_pre_cache:
|
||||
return OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE;
|
||||
default:
|
||||
return OAKENGINE_APP_RUN_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
int oakengine_app_fullscreen(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return app_core()->core_params().fullscreen() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_app_startup_project(char *buf, int buf_size)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(app_core()->core_params().startup_project(), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
void *oakengine_app_undo_stack(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return nullptr;
|
||||
}
|
||||
return app_core()->undo_stack();
|
||||
}
|
||||
|
||||
int oakengine_app_tool(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return int(app_core()->tool());
|
||||
}
|
||||
|
||||
int oakengine_app_set_tool(int tool)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (tool < 0 || tool >= int(olive::Tool::k_count)) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_tool(static_cast<olive::Tool::Item>(tool));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_addable_object(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return int(app_core()->get_selected_addable_object());
|
||||
}
|
||||
|
||||
int oakengine_app_set_addable_object(int object)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (object < 0 || object >= int(olive::Tool::k_addable_count)) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_selected_addable_object(
|
||||
static_cast<olive::Tool::AddableObject>(object));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_selected_transition(char *buf, int buf_size)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(app_core()->get_selected_transition(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_app_set_selected_transition(const char *id)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_selected_transition_object(
|
||||
id ? QString::fromUtf8(id) : QString());
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_snapping(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return app_core()->snapping() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_app_set_snapping(int enabled)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_snapping(enabled != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_timecode_display(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return int(app_core()->get_timecode_display());
|
||||
}
|
||||
|
||||
int oakengine_app_set_timecode_display(int display)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (display < 0 ||
|
||||
display > int(olive::core::Timecode::k_milliseconds)) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_timecode_display(
|
||||
static_cast<olive::core::Timecode::Display>(display));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_recent_projects_count(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return int(app_core()->get_recent_projects().size());
|
||||
}
|
||||
|
||||
int oakengine_app_recent_project_at(int index, char *buf, int buf_size)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QStringList &recent = app_core()->get_recent_projects();
|
||||
if (index < 0 || index >= recent.size()) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
return string_to_buf(recent.at(index), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_app_remove_recent_project(int index)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (index < 0 || index >= app_core()->get_recent_projects().size()) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
app_core()->remove_recently_opened_project(index);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_clear_recent_projects(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->clear_open_recent_list();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_show_status_message(const char *message, int timeout)
|
||||
{
|
||||
if (!app_core() || !message) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->show_status_bar_message(QString::fromUtf8(message), timeout);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_clear_status_message(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->clear_status_bar_message();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_set_language(const char *locale)
|
||||
{
|
||||
if (!app_core() || !locale) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
return app_core()->set_language(QString::fromUtf8(locale)) ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_app_set_autorecovery_interval(int minutes)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_autorecovery_interval(minutes);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_set_use_proxy_media(int enabled)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_use_proxy_media(enabled != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_request_pixel_sampling(int enable)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->request_pixel_sampling_in_viewers(enable != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_set_magic(int enabled)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_magic(enabled != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_is_magic_enabled(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return app_core()->is_magic_enabled() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_app_copy_to_clipboard(const char *text)
|
||||
{
|
||||
if (!app_core() || !text) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::EngineCore::copy_string_to_clipboard(QString::fromUtf8(text));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_paste_from_clipboard(char *buf, int buf_size)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(olive::EngineCore::paste_string_from_clipboard(), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakengine_app_footage_file_dialog_filter(char *buf, int buf_size)
|
||||
{
|
||||
return string_to_buf(olive::EngineCore::footage_file_dialog_filter(), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakengine_app_is_footage_extension_allowed(const char *path)
|
||||
{
|
||||
if (!path) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return olive::EngineCore::is_footage_extension_allowed(
|
||||
QString::fromUtf8(path)) ?
|
||||
1 :
|
||||
0;
|
||||
}
|
||||
|
||||
OakEngineSequence *oakengine_app_create_sequence(OakEngineProject *project,
|
||||
const char *name_format)
|
||||
{
|
||||
if (!app_core() || !project) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const QString format = name_format ?
|
||||
QString::fromUtf8(name_format) :
|
||||
QStringLiteral("Sequence %1");
|
||||
return wrap_seq(olive::EngineCore::create_new_sequence_for_project(
|
||||
format, impl(project)));
|
||||
}
|
||||
|
||||
int oakengine_app_auto_recovery_index_filename(char *buf, int buf_size)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(olive::EngineCore::get_auto_recovery_index_filename(),
|
||||
buf, buf_size);
|
||||
}
|
||||
|
||||
OakEngineProject *oakengine_app_open_project(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap(app_core()->open_project());
|
||||
}
|
||||
|
||||
int oakengine_app_create_new_project(void)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->create_new_project();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_add_open_project(OakEngineProject *project,
|
||||
int add_to_recents)
|
||||
{
|
||||
if (!app_core() || !project) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->add_open_project(impl(project), add_to_recents != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_add_open_project_from_task(void *task, int add_to_recents)
|
||||
{
|
||||
if (!app_core() || !task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
return app_core()->add_open_project_from_task(
|
||||
static_cast<olive::Task *>(task), add_to_recents != 0) ?
|
||||
1 :
|
||||
0;
|
||||
}
|
||||
|
||||
int oakengine_app_add_recovery_project_from_task(void *task)
|
||||
{
|
||||
if (!app_core() || !task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->add_recovery_project_from_task(static_cast<olive::Task *>(task));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_on_project_saved(OakEngineProject *project)
|
||||
{
|
||||
if (!app_core() || !project) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->on_project_saved(impl(project));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_app_set_active_project(OakEngineProject *project)
|
||||
{
|
||||
if (!app_core()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
app_core()->set_active_project(impl(project));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
// ---- Individual handler setter convenience wrappers ----
|
||||
|
||||
int oakengine_app_set_confirm_image_sequence_handler(
|
||||
int (*fn)(const char *filename, void *userdata), void *userdata)
|
||||
{
|
||||
OakEngineAppCallbacks cb = g_callbacks;
|
||||
cb.userdata = userdata;
|
||||
cb.confirm_image_sequence = fn;
|
||||
return oakengine_app_set_callbacks(&cb);
|
||||
}
|
||||
|
||||
int oakengine_app_set_relink_handler(
|
||||
int (*fn)(OakEngineFootage **footage, int count, void *userdata),
|
||||
void *userdata)
|
||||
{
|
||||
OakEngineAppCallbacks cb = g_callbacks;
|
||||
cb.userdata = userdata;
|
||||
cb.relink_footage = fn;
|
||||
return oakengine_app_set_callbacks(&cb);
|
||||
}
|
||||
|
||||
int oakengine_app_set_save_project_handler(
|
||||
void (*fn)(const char *override_filename, void *userdata), void *userdata)
|
||||
{
|
||||
OakEngineAppCallbacks cb = g_callbacks;
|
||||
cb.userdata = userdata;
|
||||
cb.save_project = fn;
|
||||
return oakengine_app_set_callbacks(&cb);
|
||||
}
|
||||
|
||||
int oakengine_app_set_close_project_handler(
|
||||
int (*fn)(void *userdata), void *userdata)
|
||||
{
|
||||
OakEngineAppCallbacks cb = g_callbacks;
|
||||
cb.userdata = userdata;
|
||||
cb.close_project = fn;
|
||||
return oakengine_app_set_callbacks(&cb);
|
||||
}
|
||||
|
||||
int oakengine_app_set_load_layout_handler(
|
||||
void (*fn)(const void *layout, void *userdata), void *userdata)
|
||||
{
|
||||
OakEngineAppCallbacks cb = g_callbacks;
|
||||
cb.userdata = userdata;
|
||||
cb.load_layout = fn;
|
||||
return oakengine_app_set_callbacks(&cb);
|
||||
}
|
||||
|
||||
// ---- void*-based convenience overloads ----
|
||||
|
||||
int oakengine_app_get_auto_recovery_index_filename(char *buf, int buf_size)
|
||||
{
|
||||
return oakengine_app_auto_recovery_index_filename(buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_app_remove_recently_opened_project(int index)
|
||||
{
|
||||
return oakengine_app_remove_recent_project(index);
|
||||
}
|
||||
|
||||
int oakengine_app_on_project_saved_vp(void *project)
|
||||
{
|
||||
return oakengine_app_on_project_saved(
|
||||
reinterpret_cast<OakEngineProject *>(project));
|
||||
}
|
||||
|
||||
int oakengine_app_set_active_project_vp(void *project)
|
||||
{
|
||||
return oakengine_app_set_active_project(
|
||||
reinterpret_cast<OakEngineProject *>(project));
|
||||
}
|
||||
|
||||
int oakengine_app_add_open_project_vp(void *project, int add_to_recents)
|
||||
{
|
||||
return oakengine_app_add_open_project(
|
||||
reinterpret_cast<OakEngineProject *>(project), add_to_recents);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,458 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/audio.h"
|
||||
#include "oakengine/encoding.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "audio/audiomanager.h"
|
||||
#include "audio/audioprocessor.h"
|
||||
#include "audio/audiosynchronizer.h"
|
||||
#include "audio/audiowaveformsync.h"
|
||||
#include "olive/core/oakcore/audioparams.h"
|
||||
#include "olive/core/render/audioparams.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int write_string(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf8 = s.toUtf8();
|
||||
const int len = int(utf8.size());
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = qMin(len, buf_size - 1);
|
||||
std::memcpy(buf, utf8.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
olive::AudioManager *manager()
|
||||
{
|
||||
return olive::AudioManager::instance();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakengine_audio_create_instance(void)
|
||||
{
|
||||
olive::AudioManager::create_instance();
|
||||
return manager() ? OAKENGINE_OK : OAKENGINE_E_FAILED;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_destroy_instance(void)
|
||||
{
|
||||
olive::AudioManager::destroy_instance();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_audio_manager_handle(void)
|
||||
{
|
||||
return manager();
|
||||
}
|
||||
|
||||
extern "C" int64_t oakengine_audio_get_output_device(void)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
return static_cast<int64_t>(m->get_output_device());
|
||||
}
|
||||
return -1; // paNoDevice
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_set_output_device(int64_t device)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
m->set_output_device(static_cast<PaDeviceIndex>(device));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
extern "C" int64_t oakengine_audio_get_input_device(void)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
return static_cast<int64_t>(m->get_input_device());
|
||||
}
|
||||
return -1; // paNoDevice
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_set_input_device(int64_t device)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
m->set_input_device(static_cast<PaDeviceIndex>(device));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_hard_reset(void)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
m->hard_reset();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_clear_buffered_output(void)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
m->clear_buffered_output();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_push_to_output(const OakAudioParams *params,
|
||||
const char *samples,
|
||||
int64_t samples_size,
|
||||
char *error_buf,
|
||||
int error_buf_size)
|
||||
{
|
||||
if (!params || !samples || samples_size < 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::AudioManager *m = manager();
|
||||
if (!m) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
// The C++ AudioParams wrapper owns the OakAudioParams handle; the caller
|
||||
// keeps ownership of `params`, so copy before wrapping.
|
||||
const olive::core::AudioParams cpp_params =
|
||||
olive::core::AudioParams::from_handle(
|
||||
oakcore_audioparams_copy(params));
|
||||
|
||||
QString error;
|
||||
const QByteArray data = QByteArray::fromRawData(samples,
|
||||
static_cast<int>(samples_size));
|
||||
if (!m->push_to_output(cpp_params, data, &error)) {
|
||||
write_string(error, error_buf, error_buf_size);
|
||||
return OAKENGINE_E_FAILED;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_stop_recording(void)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
m->stop_recording();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_stop_output(void)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
m->stop_output();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_reset_output_clock(void)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
m->reset_output_clock();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_set_output_notify_interval(int64_t bytes)
|
||||
{
|
||||
if (olive::AudioManager *m = manager()) {
|
||||
m->set_output_notify_interval(static_cast<int>(bytes));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_start_recording(
|
||||
OakEngineEncodingParams *params, char *error_buf, int error_buf_size)
|
||||
{
|
||||
// Delegate to the encoding-family implementation which handles the
|
||||
// OakEngineEncodingParams -> EncodingParams conversion internally.
|
||||
return oakengine_encoding_start_audio_recording(params, error_buf,
|
||||
error_buf_size);
|
||||
}
|
||||
|
||||
|
||||
extern "C" int oakengine_audio_estimate_envelope_offset(
|
||||
const double *reference, int reference_len,
|
||||
const double *candidate, int candidate_len,
|
||||
const bool *reference_valid, int reference_valid_len,
|
||||
const bool *candidate_valid, int candidate_valid_len,
|
||||
uint64_t window_samples, int64_t max_offset_windows,
|
||||
oak_audio_waveform_offset *out)
|
||||
{
|
||||
if (!out || !reference || !candidate || reference_len < 0 ||
|
||||
candidate_len < 0 || !window_samples) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
if ((reference_valid && reference_valid_len != reference_len) ||
|
||||
(candidate_valid && candidate_valid_len != candidate_len)) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
QVector<double> ref(reference_len);
|
||||
std::copy(reference, reference + reference_len, ref.begin());
|
||||
QVector<double> cand(candidate_len);
|
||||
std::copy(candidate, candidate + candidate_len, cand.begin());
|
||||
|
||||
QVector<bool> ref_valid;
|
||||
if (reference_valid) {
|
||||
ref_valid.resize(reference_valid_len);
|
||||
std::copy(reference_valid, reference_valid + reference_valid_len,
|
||||
ref_valid.begin());
|
||||
}
|
||||
|
||||
QVector<bool> cand_valid;
|
||||
if (candidate_valid) {
|
||||
cand_valid.resize(candidate_valid_len);
|
||||
std::copy(candidate_valid, candidate_valid + candidate_valid_len,
|
||||
cand_valid.begin());
|
||||
}
|
||||
|
||||
const olive::AudioWaveformSync::OffsetResult result =
|
||||
olive::AudioWaveformSync::estimate_envelope_offset(
|
||||
ref, cand, ref_valid, cand_valid, window_samples, max_offset_windows);
|
||||
|
||||
out->offset_samples = result.offset_samples;
|
||||
out->confidence = result.confidence;
|
||||
out->valid = result.valid ? 1 : 0;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_estimate_stretch_and_offset(
|
||||
const double *reference, int reference_len,
|
||||
const double *candidate, int candidate_len,
|
||||
const bool *reference_valid, int reference_valid_len,
|
||||
const bool *candidate_valid, int candidate_valid_len,
|
||||
uint64_t window_samples, int64_t max_offset_windows,
|
||||
double min_rate, double max_rate, double rate_step,
|
||||
oak_audio_waveform_stretch_offset *out)
|
||||
{
|
||||
if (!out || !reference || !candidate || reference_len < 0 ||
|
||||
candidate_len < 0 || !window_samples) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
if ((reference_valid && reference_valid_len != reference_len) ||
|
||||
(candidate_valid && candidate_valid_len != candidate_len)) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
QVector<double> ref(reference_len);
|
||||
std::copy(reference, reference + reference_len, ref.begin());
|
||||
QVector<double> cand(candidate_len);
|
||||
std::copy(candidate, candidate + candidate_len, cand.begin());
|
||||
|
||||
QVector<bool> ref_valid;
|
||||
if (reference_valid) {
|
||||
ref_valid.resize(reference_valid_len);
|
||||
std::copy(reference_valid, reference_valid + reference_valid_len,
|
||||
ref_valid.begin());
|
||||
}
|
||||
|
||||
QVector<bool> cand_valid;
|
||||
if (candidate_valid) {
|
||||
cand_valid.resize(candidate_valid_len);
|
||||
std::copy(candidate_valid, candidate_valid + candidate_valid_len,
|
||||
cand_valid.begin());
|
||||
}
|
||||
|
||||
const olive::AudioWaveformSync::StretchOffsetResult result =
|
||||
olive::AudioWaveformSync::estimate_stretch_and_offset(
|
||||
ref, cand, ref_valid, cand_valid, window_samples, max_offset_windows,
|
||||
min_rate, max_rate, rate_step);
|
||||
|
||||
out->rate = result.rate;
|
||||
out->offset_samples = result.offset_samples;
|
||||
out->confidence = result.confidence;
|
||||
out->valid = result.valid ? 1 : 0;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_sync_place_by_source_time(
|
||||
const oak_audio_sync_source_clip *reference,
|
||||
const oak_audio_sync_source_clip *candidate,
|
||||
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
|
||||
oak_audio_sync_placement *out)
|
||||
{
|
||||
if (!reference || !candidate || !out) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::AudioSynchronizer::SourceClip ref;
|
||||
ref.source_start_time = olive::core::Rational(
|
||||
reference->source_start_time_num, reference->source_start_time_den);
|
||||
ref.media_in =
|
||||
olive::core::Rational(reference->media_in_num, reference->media_in_den);
|
||||
ref.has_source_start_time = reference->has_source_start_time != 0;
|
||||
|
||||
olive::AudioSynchronizer::SourceClip cand;
|
||||
cand.source_start_time = olive::core::Rational(
|
||||
candidate->source_start_time_num, candidate->source_start_time_den);
|
||||
cand.media_in =
|
||||
olive::core::Rational(candidate->media_in_num, candidate->media_in_den);
|
||||
cand.has_source_start_time = candidate->has_source_start_time != 0;
|
||||
|
||||
const olive::core::Rational timeline_in(reference_timeline_in_num,
|
||||
reference_timeline_in_den);
|
||||
const olive::AudioSynchronizer::Placement placement =
|
||||
olive::AudioSynchronizer::place_by_source_time(ref, cand, timeline_in);
|
||||
|
||||
out->timeline_in_num = placement.timeline_in.numerator();
|
||||
out->timeline_in_den = placement.timeline_in.denominator();
|
||||
out->valid = placement.valid ? 1 : 0;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_sync_place_by_waveform_offset(
|
||||
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
|
||||
int64_t candidate_offset_samples, int sample_rate,
|
||||
oak_audio_sync_placement *out)
|
||||
{
|
||||
if (!out || sample_rate <= 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
const olive::core::Rational timeline_in(reference_timeline_in_num,
|
||||
reference_timeline_in_den);
|
||||
const olive::AudioSynchronizer::Placement placement =
|
||||
olive::AudioSynchronizer::place_by_waveform_offset(
|
||||
timeline_in, candidate_offset_samples, sample_rate);
|
||||
|
||||
out->timeline_in_num = placement.timeline_in.numerator();
|
||||
out->timeline_in_den = placement.timeline_in.denominator();
|
||||
out->valid = placement.valid ? 1 : 0;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
/* ---- Audio format processor (R6 P5) ------------------------------------- */
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::core::AudioParams params_from_c(const OakAudioParams *p)
|
||||
{
|
||||
// AudioParams takes ownership of the handle, so hand it a copy.
|
||||
return olive::core::AudioParams::from_handle(oakcore_audioparams_copy(p));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct OakEngineAudioProcessor {
|
||||
olive::AudioProcessor proc;
|
||||
|
||||
// Holds the packed output of the most recent convert() so the caller can
|
||||
// borrow the bytes across the C boundary.
|
||||
olive::AudioProcessor::Buffer buf;
|
||||
};
|
||||
|
||||
extern "C" OakEngineAudioProcessor *oakengine_audio_processor_create(void)
|
||||
{
|
||||
return new (std::nothrow) OakEngineAudioProcessor();
|
||||
}
|
||||
|
||||
extern "C" void oakengine_audio_processor_free(OakEngineAudioProcessor *p)
|
||||
{
|
||||
delete p;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_processor_open(OakEngineAudioProcessor *p,
|
||||
const OakAudioParams *from,
|
||||
const OakAudioParams *to,
|
||||
double tempo)
|
||||
{
|
||||
if (!p || !from || !to) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
const olive::core::AudioParams cpp_from = params_from_c(from);
|
||||
const olive::core::AudioParams cpp_to = params_from_c(to);
|
||||
return p->proc.open(cpp_from, cpp_to, tempo) ? OAKENGINE_OK
|
||||
: OAKENGINE_E_FAILED;
|
||||
}
|
||||
|
||||
extern "C" void oakengine_audio_processor_close(OakEngineAudioProcessor *p)
|
||||
{
|
||||
if (p) {
|
||||
p->buf.clear();
|
||||
p->proc.close();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_processor_is_open(OakEngineAudioProcessor *p)
|
||||
{
|
||||
return (p && p->proc.is_open()) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_audio_processor_convert(OakEngineAudioProcessor *p,
|
||||
float **in, int nb_in_samples,
|
||||
const void **out_data,
|
||||
int *out_size)
|
||||
{
|
||||
if (!p) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
if (out_data) {
|
||||
*out_data = nullptr;
|
||||
}
|
||||
if (out_size) {
|
||||
*out_size = 0;
|
||||
}
|
||||
|
||||
p->buf.clear();
|
||||
const int r = p->proc.convert(in, nb_in_samples, &p->buf);
|
||||
if (r < 0) {
|
||||
return r;
|
||||
}
|
||||
|
||||
if (!p->buf.empty()) {
|
||||
if (out_data) {
|
||||
*out_data = p->buf.at(0).constData();
|
||||
}
|
||||
if (out_size) {
|
||||
*out_size = p->buf.at(0).size();
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
extern "C" OakAudioParams *oakengine_audio_processor_output_params(
|
||||
OakEngineAudioProcessor *p)
|
||||
{
|
||||
if (!p || !p->proc.is_open()) {
|
||||
return nullptr;
|
||||
}
|
||||
return oakcore_audioparams_copy(p->proc.to().handle());
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/color.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "colorinternal.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/project.h"
|
||||
#include "render/job/colortransformjob.h"
|
||||
#include "render/previewautocacher.h"
|
||||
#include "render/rendermanager.h"
|
||||
|
||||
// The OakEngineColorProcessor handle layout is shared with the other capi
|
||||
// translation units via colorinternal.h.
|
||||
struct OakEngineColorConfig {
|
||||
ocio::ConstConfigRcPtr ptr;
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// buf/size convention: returns the would-be length excluding the NUL.
|
||||
int string_to_buf(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf = s.toUtf8();
|
||||
if (buf && buf_size > 0) {
|
||||
snprintf(buf, size_t(buf_size), "%s", utf.constData());
|
||||
}
|
||||
return int(utf.size());
|
||||
}
|
||||
|
||||
thread_local QString g_last_error;
|
||||
|
||||
void set_error(const QString &error)
|
||||
{
|
||||
g_last_error = error;
|
||||
}
|
||||
|
||||
olive::ColorManager *impl(const OakEngineColorManager *h)
|
||||
{
|
||||
return reinterpret_cast<olive::ColorManager *>(
|
||||
const_cast<OakEngineColorManager *>(h));
|
||||
}
|
||||
|
||||
olive::ColorTransform to_cpp(const oak_color_transform &t)
|
||||
{
|
||||
if (t.is_display) {
|
||||
return olive::ColorTransform(
|
||||
t.output ? QString::fromUtf8(t.output) : QString(),
|
||||
t.view ? QString::fromUtf8(t.view) : QString(),
|
||||
t.look ? QString::fromUtf8(t.look) : QString());
|
||||
}
|
||||
return olive::ColorTransform(t.output ? QString::fromUtf8(t.output) :
|
||||
QString());
|
||||
}
|
||||
|
||||
// The engine's list accessors dereference the config unconditionally;
|
||||
// guard here so a manager whose config failed to load yields empty lists
|
||||
// instead of crashing.
|
||||
bool has_config(const olive::ColorManager *mgr)
|
||||
{
|
||||
return mgr && mgr->get_config();
|
||||
}
|
||||
|
||||
QString list_at(const QStringList &l, int index)
|
||||
{
|
||||
return (index >= 0 && index < l.size()) ? l.at(index) : QString();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
int oakengine_color_last_error(char *buf, int buf_size)
|
||||
{
|
||||
return string_to_buf(g_last_error, buf, buf_size);
|
||||
}
|
||||
|
||||
OakEngineColorManager *
|
||||
oakengine_color_manager_from_project(OakEngineProject *project)
|
||||
{
|
||||
if (!project) {
|
||||
return nullptr;
|
||||
}
|
||||
auto *p = reinterpret_cast<olive::Project *>(project);
|
||||
return reinterpret_cast<OakEngineColorManager *>(p->color_manager());
|
||||
}
|
||||
|
||||
int oakengine_color_manager_get_config_filename(
|
||||
const OakEngineColorManager *mgr, char *buf, int buf_size)
|
||||
{
|
||||
if (!mgr) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(mgr)->get_config_filename(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_set_config_filename(OakEngineColorManager *mgr,
|
||||
const char *filename)
|
||||
{
|
||||
if (!mgr || !filename) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(mgr)->set_config_filename(QString::fromUtf8(filename));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_color_manager_colorspace_count(const OakEngineColorManager *mgr)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return 0;
|
||||
}
|
||||
return impl(mgr)->list_available_colorspaces().size();
|
||||
}
|
||||
|
||||
int oakengine_color_manager_colorspace_at(const OakEngineColorManager *mgr,
|
||||
int index, char *buf, int buf_size)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString s =
|
||||
list_at(impl(mgr)->list_available_colorspaces(), index);
|
||||
if (s.isNull()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(s, buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_display_count(const OakEngineColorManager *mgr)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return 0;
|
||||
}
|
||||
return impl(mgr)->list_available_displays().size();
|
||||
}
|
||||
|
||||
int oakengine_color_manager_display_at(const OakEngineColorManager *mgr,
|
||||
int index, char *buf, int buf_size)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString s = list_at(impl(mgr)->list_available_displays(), index);
|
||||
if (s.isNull()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(s, buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_view_count(const OakEngineColorManager *mgr,
|
||||
const char *display)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return 0;
|
||||
}
|
||||
return impl(mgr)
|
||||
->list_available_views(display ? QString::fromUtf8(display) : QString())
|
||||
.size();
|
||||
}
|
||||
|
||||
int oakengine_color_manager_view_at(const OakEngineColorManager *mgr,
|
||||
const char *display, int index, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString s = list_at(
|
||||
impl(mgr)->list_available_views(display ? QString::fromUtf8(display) :
|
||||
QString()),
|
||||
index);
|
||||
if (s.isNull()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(s, buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_look_count(const OakEngineColorManager *mgr)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return 0;
|
||||
}
|
||||
return impl(mgr)->list_available_looks().size();
|
||||
}
|
||||
|
||||
int oakengine_color_manager_look_at(const OakEngineColorManager *mgr,
|
||||
int index, char *buf, int buf_size)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString s = list_at(impl(mgr)->list_available_looks(), index);
|
||||
if (s.isNull()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(s, buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_default_display(const OakEngineColorManager *mgr,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(mgr)->get_default_display(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_default_view(const OakEngineColorManager *mgr,
|
||||
const char *display, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!has_config(impl(mgr))) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(
|
||||
impl(mgr)->get_default_view(display ? QString::fromUtf8(display) :
|
||||
QString()),
|
||||
buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_default_input_color_space(
|
||||
const OakEngineColorManager *mgr, char *buf, int buf_size)
|
||||
{
|
||||
if (!mgr) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(mgr)->get_default_input_color_space(), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_set_default_input_color_space(
|
||||
OakEngineColorManager *mgr, const char *colorspace)
|
||||
{
|
||||
if (!mgr || !colorspace) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(mgr)->set_default_input_color_space(QString::fromUtf8(colorspace));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_color_manager_reference_color_space(
|
||||
const OakEngineColorManager *mgr, char *buf, int buf_size)
|
||||
{
|
||||
if (!mgr) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(mgr)->get_reference_color_space(), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_default_luma_coefs(
|
||||
const OakEngineColorManager *mgr, double *rgb)
|
||||
{
|
||||
if (!mgr || !rgb) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(mgr)->get_default_luma_coefs(rgb);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_color_manager_compliant_color_space(
|
||||
const OakEngineColorManager *mgr, const char *name, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!has_config(impl(mgr)) || !name) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(
|
||||
impl(mgr)->get_compliant_color_space(QString::fromUtf8(name)), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakengine_color_manager_compliant_transform(
|
||||
const OakEngineColorManager *mgr, const oak_color_transform *in,
|
||||
int force_display, int *out_is_display, char *out_output,
|
||||
int output_size, char *out_view, int view_size, char *out_look,
|
||||
int look_size)
|
||||
{
|
||||
if (!has_config(impl(mgr)) || !in) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::ColorTransform compliant =
|
||||
impl(mgr)->get_compliant_color_space(to_cpp(*in), force_display != 0);
|
||||
if (out_is_display) {
|
||||
*out_is_display = compliant.is_display() ? 1 : 0;
|
||||
}
|
||||
string_to_buf(compliant.output(), out_output, output_size);
|
||||
string_to_buf(compliant.view(), out_view, view_size);
|
||||
string_to_buf(compliant.look(), out_look, look_size);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
OakEngineColorConfig *oakengine_color_config_load_default(void)
|
||||
{
|
||||
try {
|
||||
ocio::ConstConfigRcPtr c = olive::ColorManager::get_default_config();
|
||||
if (!c) {
|
||||
set_error(QStringLiteral("no default OCIO config available"));
|
||||
return nullptr;
|
||||
}
|
||||
set_error(QString());
|
||||
return new OakEngineColorConfig{std::move(c)};
|
||||
} catch (ocio::Exception &e) {
|
||||
set_error(QString::fromUtf8(e.what()));
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
OakEngineColorConfig *oakengine_color_config_load_file(const char *filename)
|
||||
{
|
||||
if (!filename) {
|
||||
set_error(QStringLiteral("no filename given"));
|
||||
return nullptr;
|
||||
}
|
||||
try {
|
||||
ocio::ConstConfigRcPtr c =
|
||||
olive::ColorManager::create_config_from_file(
|
||||
QString::fromUtf8(filename));
|
||||
set_error(QString());
|
||||
return new OakEngineColorConfig{std::move(c)};
|
||||
} catch (ocio::Exception &e) {
|
||||
set_error(QString::fromUtf8(e.what()));
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void oakengine_color_config_free(OakEngineColorConfig *config)
|
||||
{
|
||||
delete config;
|
||||
}
|
||||
|
||||
int oakengine_color_config_colorspace_count(const OakEngineColorConfig *config)
|
||||
{
|
||||
if (!config || !config->ptr) {
|
||||
return 0;
|
||||
}
|
||||
return olive::ColorManager::list_available_colorspaces(config->ptr).size();
|
||||
}
|
||||
|
||||
int oakengine_color_config_colorspace_at(const OakEngineColorConfig *config,
|
||||
int index, char *buf, int buf_size)
|
||||
{
|
||||
if (!config || !config->ptr) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString s = list_at(
|
||||
olive::ColorManager::list_available_colorspaces(config->ptr), index);
|
||||
if (s.isNull()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(s, buf, buf_size);
|
||||
}
|
||||
|
||||
OakEngineColorProcessor *oakengine_color_processor_create(
|
||||
const OakEngineColorManager *mgr, const char *input,
|
||||
const oak_color_transform *dest, int direction)
|
||||
{
|
||||
if (!mgr || !input || !dest ||
|
||||
(direction != OAKENGINE_COLOR_PROCESSOR_NORMAL &&
|
||||
direction != OAKENGINE_COLOR_PROCESSOR_INVERSE)) {
|
||||
return nullptr;
|
||||
}
|
||||
// ColorProcessor catches OCIO failures internally and leaves the
|
||||
// processor null (see engine/render/colorprocessor.cpp), so this never
|
||||
// throws; validity is reported through is_valid().
|
||||
auto *proc = new OakEngineColorProcessor;
|
||||
proc->ptr = olive::ColorProcessor::create(
|
||||
impl(mgr), QString::fromUtf8(input), to_cpp(*dest),
|
||||
direction == OAKENGINE_COLOR_PROCESSOR_INVERSE ?
|
||||
olive::ColorProcessor::k_inverse :
|
||||
olive::ColorProcessor::k_normal);
|
||||
return proc;
|
||||
}
|
||||
|
||||
void oakengine_color_processor_free(OakEngineColorProcessor *proc)
|
||||
{
|
||||
delete proc;
|
||||
}
|
||||
|
||||
int oakengine_color_processor_is_valid(const OakEngineColorProcessor *proc)
|
||||
{
|
||||
return (proc && proc->ptr && proc->ptr->get_processor()) ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_color_processor_convert_color(
|
||||
const OakEngineColorProcessor *proc, const double *in_rgba,
|
||||
double *out_rgba)
|
||||
{
|
||||
if (!proc || !proc->ptr || !in_rgba || !out_rgba) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::Color out = proc->ptr->convert_color(
|
||||
olive::Color(in_rgba[0], in_rgba[1], in_rgba[2], in_rgba[3]));
|
||||
out_rgba[0] = out.red();
|
||||
out_rgba[1] = out.green();
|
||||
out_rgba[2] = out.blue();
|
||||
out_rgba[3] = out.alpha();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_color_processor_id(const OakEngineColorProcessor *proc,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!proc || !proc->ptr) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const char *id = proc->ptr->id();
|
||||
const int len = id ? int(strlen(id)) : 0;
|
||||
if (buf && buf_size > 0) {
|
||||
snprintf(buf, size_t(buf_size), "%s", id ? id : "");
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
int oakengine_color_transform_job_set_processor(
|
||||
void *job, const OakEngineColorProcessor *proc)
|
||||
{
|
||||
if (!job) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
auto *j = reinterpret_cast<olive::ColorTransformJob *>(job);
|
||||
j->set_color_processor(proc ? proc->ptr : olive::ColorProcessorPtr());
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,38 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAKENGINE_COLORINTERNAL_H
|
||||
#define OAKENGINE_COLORINTERNAL_H
|
||||
|
||||
// Internal (not installed) shared definition of the opaque color-processor
|
||||
// handle between color.cpp and the other capi translation units. The
|
||||
// public header (oakengine/color.h) only forward-declares
|
||||
// OakEngineColorProcessor; capi code that needs to unwrap the handle (e.g.
|
||||
// renderer.cpp feeding the render cacher) includes this header.
|
||||
|
||||
#include "render/colorprocessor.h"
|
||||
|
||||
// Owned handle layout: the opaque C type is a heap box around the engine's
|
||||
// shared pointer (matching the refcounting the C++ API uses).
|
||||
struct OakEngineColorProcessor {
|
||||
olive::ColorProcessorPtr ptr;
|
||||
};
|
||||
|
||||
#endif // OAKENGINE_COLORINTERNAL_H
|
||||
@@ -0,0 +1,130 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/config.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "config/config.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int write_string(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf8 = s.toUtf8();
|
||||
const int len = int(utf8.size());
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = qMin(len, buf_size - 1);
|
||||
std::memcpy(buf, utf8.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
oakengine_config_error_fn g_error_fn = nullptr;
|
||||
void *g_error_userdata = nullptr;
|
||||
|
||||
void error_handler(const QString &title, const QString &message)
|
||||
{
|
||||
if (g_error_fn) {
|
||||
const QByteArray t = title.toUtf8();
|
||||
const QByteArray m = message.toUtf8();
|
||||
g_error_fn(t.constData(), m.constData(), g_error_userdata);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakengine_config_load(void)
|
||||
{
|
||||
olive::Config::load();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_config_save(void)
|
||||
{
|
||||
olive::Config::save();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_config_get_string(const char *key, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!key) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QVariant v = olive::Config::current()[QString::fromUtf8(key)];
|
||||
const QString s = v.toString();
|
||||
return write_string(s, buf, buf_size);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_config_set_string(const char *key,
|
||||
const char *value)
|
||||
{
|
||||
if (!key) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Config::current()[QString::fromUtf8(key)] =
|
||||
QString::fromUtf8(value ? value : "");
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int64_t oakengine_config_get_int(const char *key,
|
||||
int64_t default_value)
|
||||
{
|
||||
if (!key) {
|
||||
return default_value;
|
||||
}
|
||||
const QVariant v = olive::Config::current()[QString::fromUtf8(key)];
|
||||
bool ok = false;
|
||||
const qlonglong val = v.toLongLong(&ok);
|
||||
return ok ? static_cast<int64_t>(val) : default_value;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_config_set_int(const char *key, int64_t value)
|
||||
{
|
||||
if (!key) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Config::current()[QString::fromUtf8(key)] =
|
||||
static_cast<qlonglong>(value);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_config_set_error_handler(
|
||||
oakengine_config_error_fn fn, void *userdata)
|
||||
{
|
||||
g_error_fn = fn;
|
||||
g_error_userdata = userdata;
|
||||
olive::Config::set_error_handler(fn ? error_handler : nullptr);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_config_report_error(const char *title,
|
||||
const char *message)
|
||||
{
|
||||
olive::Config::report_error(QString::fromUtf8(title ? title : ""),
|
||||
QString::fromUtf8(message ? message : ""));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/disk.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include "node/project.h"
|
||||
#include "render/diskmanager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int write_string(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf8 = s.toUtf8();
|
||||
const int len = int(utf8.size());
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = qMin(len, buf_size - 1);
|
||||
std::memcpy(buf, utf8.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
olive::DiskManager *manager()
|
||||
{
|
||||
return olive::DiskManager::instance();
|
||||
}
|
||||
|
||||
olive::DiskCacheFolder *folder_from_path(olive::DiskManager *m,
|
||||
const char *path)
|
||||
{
|
||||
if (!m) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!path || std::strlen(path) == 0) {
|
||||
return m->get_default_cache_folder();
|
||||
}
|
||||
return m->get_open_folder(QString::fromUtf8(path));
|
||||
}
|
||||
|
||||
struct SettingsHandlerState {
|
||||
oakengine_disk_settings_fn fn = nullptr;
|
||||
void *userdata = nullptr;
|
||||
};
|
||||
|
||||
SettingsHandlerState g_settings_handler;
|
||||
|
||||
void cpp_settings_handler(olive::DiskCacheFolder *folder, QWidget *parent)
|
||||
{
|
||||
if (!g_settings_handler.fn || !folder) {
|
||||
return;
|
||||
}
|
||||
const QByteArray path = folder->get_path().toUtf8();
|
||||
g_settings_handler.fn(path.constData(), parent, g_settings_handler.userdata);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakengine_disk_create_instance(void)
|
||||
{
|
||||
olive::DiskManager::create_instance();
|
||||
return manager() ? OAKENGINE_OK : OAKENGINE_E_FAILED;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_disk_destroy_instance(void)
|
||||
{
|
||||
olive::DiskManager::destroy_instance();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_disk_set_settings_handler(
|
||||
oakengine_disk_settings_fn fn, void *userdata)
|
||||
{
|
||||
g_settings_handler.fn = fn;
|
||||
g_settings_handler.userdata = userdata;
|
||||
|
||||
olive::DiskManager::set_show_disk_cache_settings_handler(
|
||||
fn ? cpp_settings_handler : olive::DiskManager::ShowDiskCacheSettingsHandler{});
|
||||
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_disk_show_settings_dialog(const char *path,
|
||||
void *parent_window)
|
||||
{
|
||||
olive::DiskManager *m = manager();
|
||||
if (!m) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
olive::DiskCacheFolder *folder = folder_from_path(m, path);
|
||||
if (!folder) {
|
||||
return OAKENGINE_E_FAILED;
|
||||
}
|
||||
|
||||
m->show_disk_cache_settings_dialog(folder,
|
||||
static_cast<QWidget *>(parent_window));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_disk_show_change_confirmation_dialog(
|
||||
void *parent_window)
|
||||
{
|
||||
return olive::DiskManager::show_disk_cache_change_confirmation_dialog(
|
||||
static_cast<QWidget *>(parent_window))
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_disk_clear_cache(const char *path)
|
||||
{
|
||||
olive::DiskManager *m = manager();
|
||||
if (!m) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
olive::DiskCacheFolder *folder = folder_from_path(m, path);
|
||||
if (!folder) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m->clear_disk_cache(folder->get_path()) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_disk_get_default_cache_path(char *buf, int buf_size)
|
||||
{
|
||||
olive::DiskManager *m = manager();
|
||||
if (!m) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
return write_string(m->get_default_cache_path(), buf, buf_size);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_disk_set_default_cache_path(const char *path)
|
||||
{
|
||||
if (!path) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::DiskManager *m = manager();
|
||||
if (!m) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
m->get_default_cache_folder()->set_path(QString::fromUtf8(path));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_disk_get_open_folder(const char *path)
|
||||
{
|
||||
return folder_from_path(manager(), path);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_disk_invalidate_project(OakEngineProject *project)
|
||||
{
|
||||
olive::DiskManager *m = manager();
|
||||
if (!m) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
emit m->invalidate_project(reinterpret_cast<olive::Project *>(project));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/display.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QOpenGLContext>
|
||||
#include <QString>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "render/job/colortransformjob.h"
|
||||
#include "render/opengl/openglrenderer.h"
|
||||
#include "render/renderer.h"
|
||||
#include "render/texture.h"
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
#include "render/backend/dynamicrenderer.h"
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
|
||||
void *oakengine_display_renderer_create_dynamic(const char *backend_name,
|
||||
void *parent)
|
||||
{
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
QObject *p = static_cast<QObject *>(parent);
|
||||
auto *dyn = new olive::DynamicRenderer(
|
||||
QString::fromUtf8(backend_name ? backend_name : ""), p);
|
||||
if (dyn->load()) {
|
||||
return dyn;
|
||||
}
|
||||
// Backend library failed to load: drop it so the caller can fall back
|
||||
// to the built-in OpenGL renderer.
|
||||
delete dyn;
|
||||
return nullptr;
|
||||
#else
|
||||
(void)backend_name;
|
||||
(void)parent;
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void *oakengine_display_renderer_create_opengl(void *parent)
|
||||
{
|
||||
return new olive::OpenGLRenderer(static_cast<QObject *>(parent));
|
||||
}
|
||||
|
||||
int oakengine_display_renderer_init(void *renderer, void *gl_context)
|
||||
{
|
||||
olive::Renderer *r = static_cast<olive::Renderer *>(renderer);
|
||||
if (!r) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
if (gl_context) {
|
||||
QOpenGLContext *ctx = static_cast<QOpenGLContext *>(gl_context);
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
if (auto *dyn = dynamic_cast<olive::DynamicRenderer *>(r)) {
|
||||
dyn->init_with_open_gl_context(ctx);
|
||||
dyn->post_init();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
#endif
|
||||
auto *gl = static_cast<olive::OpenGLRenderer *>(r);
|
||||
gl->init(ctx);
|
||||
gl->post_init();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
r->init();
|
||||
r->post_init();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
void oakengine_display_renderer_destroy(void *renderer)
|
||||
{
|
||||
olive::Renderer *r = static_cast<olive::Renderer *>(renderer);
|
||||
if (!r) {
|
||||
return;
|
||||
}
|
||||
r->destroy();
|
||||
r->post_destroy();
|
||||
}
|
||||
|
||||
void oakengine_display_renderer_create_texture(void *renderer,
|
||||
const void *video_params,
|
||||
const void *pixels, int linesize,
|
||||
void *out_texture)
|
||||
{
|
||||
olive::Renderer *r = static_cast<olive::Renderer *>(renderer);
|
||||
if (!r || !video_params || !out_texture) {
|
||||
return;
|
||||
}
|
||||
const olive::VideoParams ¶ms =
|
||||
*static_cast<const olive::VideoParams *>(video_params);
|
||||
*static_cast<olive::TexturePtr *>(out_texture) =
|
||||
r->create_texture(params, pixels, linesize);
|
||||
}
|
||||
|
||||
void oakengine_display_renderer_blit_color_managed(void *renderer,
|
||||
const void *color_job,
|
||||
void *dst_texture,
|
||||
const void *video_params)
|
||||
{
|
||||
olive::Renderer *r = static_cast<olive::Renderer *>(renderer);
|
||||
if (!r || !color_job) {
|
||||
return;
|
||||
}
|
||||
const olive::ColorTransformJob &job =
|
||||
*static_cast<const olive::ColorTransformJob *>(color_job);
|
||||
olive::Texture *dst = static_cast<olive::Texture *>(dst_texture);
|
||||
if (video_params) {
|
||||
r->blit_color_managed(
|
||||
job, dst, *static_cast<const olive::VideoParams *>(video_params));
|
||||
} else if (dst) {
|
||||
r->blit_color_managed(job, dst, dst->params());
|
||||
}
|
||||
}
|
||||
|
||||
void oakengine_display_texture_upload(void *texture, void *pixels, int linesize)
|
||||
{
|
||||
olive::Texture *t = static_cast<olive::Texture *>(texture);
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
t->upload(pixels, linesize);
|
||||
}
|
||||
|
||||
void oakengine_display_texture_download(void *texture, void *pixels,
|
||||
int linesize)
|
||||
{
|
||||
olive::Texture *t = static_cast<olive::Texture *>(texture);
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
t->download(pixels, linesize);
|
||||
}
|
||||
|
||||
void oakengine_codec_frame_create(void *out_frame)
|
||||
{
|
||||
if (!out_frame) {
|
||||
return;
|
||||
}
|
||||
*static_cast<olive::FramePtr *>(out_frame) = olive::Frame::create();
|
||||
}
|
||||
|
||||
void oakengine_codec_frame_set_video_params(void *frame,
|
||||
const void *video_params)
|
||||
{
|
||||
olive::Frame *f = static_cast<olive::Frame *>(frame);
|
||||
if (!f || !video_params) {
|
||||
return;
|
||||
}
|
||||
f->set_video_params(*static_cast<const olive::VideoParams *>(video_params));
|
||||
}
|
||||
|
||||
int oakengine_codec_frame_allocate(void *frame)
|
||||
{
|
||||
olive::Frame *f = static_cast<olive::Frame *>(frame);
|
||||
if (!f) {
|
||||
return 0;
|
||||
}
|
||||
return f->allocate() ? 1 : 0;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,8 @@
|
||||
|
||||
#include "oakengine/exporter.h"
|
||||
|
||||
#include "exportinternal.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
|
||||
@@ -487,6 +489,21 @@ QString params_from_ex(const oak_export_options_ex &o,
|
||||
|
||||
} // namespace
|
||||
|
||||
int oakengine_export_render_internal(olive::Sequence *sequence,
|
||||
olive::Project *project,
|
||||
olive::EncodingParams ¶ms,
|
||||
bool prewarm_audio,
|
||||
const olive::AudioParams &prewarm_params)
|
||||
{
|
||||
return render_internal(sequence, project, params, prewarm_audio,
|
||||
prewarm_params);
|
||||
}
|
||||
|
||||
void oakengine_export_set_error_string(const QString &error)
|
||||
{
|
||||
set_error(error);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAKENGINE_EXPORTINTERNAL_H
|
||||
#define OAKENGINE_EXPORTINTERNAL_H
|
||||
|
||||
// Internal (not installed) shared declaration between the export and
|
||||
// encoding capi translation units: oakengine_export_render_with_params()
|
||||
// (declared in oakengine/encoding.h) drives the same synchronous ExportTask
|
||||
// machinery as oakengine_export_render()/_ex(), which lives in export.cpp.
|
||||
|
||||
namespace olive
|
||||
{
|
||||
class Sequence;
|
||||
class Project;
|
||||
class EncodingParams;
|
||||
namespace core
|
||||
{
|
||||
class AudioParams;
|
||||
}
|
||||
using core::AudioParams;
|
||||
}
|
||||
|
||||
// Runs the synchronous export (render_internal in export.cpp): prewarms
|
||||
// audio conforms when prewarm_audio is set, drives the ExportTask on a
|
||||
// worker thread while the calling thread pumps events. Returns
|
||||
// OAKENGINE_OK / OAKENGINE_E_STATE / OAKENGINE_E_FAILED /
|
||||
// OAKENGINE_E_CANCELLED; failure reason via oakengine_export_last_error().
|
||||
int oakengine_export_render_internal(olive::Sequence *sequence,
|
||||
olive::Project *project,
|
||||
olive::EncodingParams ¶ms,
|
||||
bool prewarm_audio,
|
||||
const olive::AudioParams &prewarm_params);
|
||||
|
||||
// Sets the export family's thread-local failure reason (read back with
|
||||
// oakengine_export_last_error()). Used by capi TUs outside export.cpp whose
|
||||
// contracts route errors through the export channel.
|
||||
class QString;
|
||||
void oakengine_export_set_error_string(const QString &error);
|
||||
|
||||
#endif // OAKENGINE_EXPORTINTERNAL_H
|
||||
+227
-13
@@ -19,6 +19,7 @@
|
||||
***/
|
||||
|
||||
#include "oakengine/footage.h"
|
||||
#include "oakengine/timeline.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
@@ -41,6 +42,7 @@
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "undo/undocommand.h"
|
||||
#include "undo/undostack.h"
|
||||
#include "undointernal.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -153,12 +155,7 @@ olive::Footage *borrowed_node(OakEngineFootage *self)
|
||||
// initialized, otherwise execute it directly.
|
||||
void push_or_run(olive::UndoCommand *command, const QString &name)
|
||||
{
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->undo_stack()->push(command, name);
|
||||
} else {
|
||||
command->redo_now();
|
||||
delete command;
|
||||
}
|
||||
oakengine_undo_push_or_run(command, name);
|
||||
}
|
||||
|
||||
// Undo commands for footage stream overrides. The engine has no undo
|
||||
@@ -546,13 +543,7 @@ OakEngineFootage *oakengine_project_import_footage(OakEngineProject *project,
|
||||
command->add_child(new olive::NodeAddCommand(p, footage));
|
||||
command->add_child(new olive::FolderAddChild(p->root(), footage));
|
||||
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->undo_stack()->push(
|
||||
command, QStringLiteral("Import Footage"));
|
||||
} else {
|
||||
command->redo_now();
|
||||
delete command;
|
||||
}
|
||||
oakengine_undo_push_or_run(command, QStringLiteral("Import Footage"));
|
||||
|
||||
auto *state = new OakEngineFootageState();
|
||||
state->borrowed = true;
|
||||
@@ -1082,4 +1073,227 @@ int oakengine_footage_colorspace_at(const OakEngineFootage *self, int index,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
/* ---- Footage extras ------------------------------------------------------- */
|
||||
|
||||
int oakengine_footage_get_filename(const OakEngineFootage *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const OakEngineFootageState *s = impl(self);
|
||||
if (!s->node) {
|
||||
// Probed footage has no node; filename is not applicable.
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(s->node->filename(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_footage_get_stream_reference(const OakEngineFootage *self,
|
||||
int flat_index, int *out_track_type,
|
||||
int *out_stream_index)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const OakEngineFootageState *s = impl(self);
|
||||
if (!s->node) {
|
||||
return OAKENGINE_E_INVALID; // probed-only footage
|
||||
}
|
||||
const int vc = video_stream_count(s);
|
||||
const int ac = audio_stream_count(s);
|
||||
if (flat_index < vc) {
|
||||
if (out_track_type) {
|
||||
*out_track_type = OAKENGINE_TRACK_TYPE_VIDEO;
|
||||
}
|
||||
if (out_stream_index) {
|
||||
*out_stream_index = flat_index;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
flat_index -= vc;
|
||||
if (flat_index < ac) {
|
||||
if (out_track_type) {
|
||||
*out_track_type = OAKENGINE_TRACK_TYPE_AUDIO;
|
||||
}
|
||||
if (out_stream_index) {
|
||||
*out_stream_index = flat_index;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
flat_index -= ac;
|
||||
const int sc = subtitle_stream_count(s);
|
||||
if (flat_index >= sc) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
if (out_track_type) {
|
||||
*out_track_type = OAKENGINE_TRACK_TYPE_SUBTITLE;
|
||||
}
|
||||
if (out_stream_index) {
|
||||
*out_stream_index = flat_index;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_describe_video_stream(const OakEngineFootage *self,
|
||||
int video_stream_index, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const OakEngineFootageState *s = impl(self);
|
||||
if (!s->node) {
|
||||
// Import-handle-only family: probe handles are rejected (probe
|
||||
// metadata is read through the oak_footage_*_info accessors).
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (video_stream_index < 0 || video_stream_index >= video_stream_count(s)) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
olive::VideoParams vp;
|
||||
if (s->node) {
|
||||
vp = s->node->get_video_params(video_stream_index);
|
||||
} else {
|
||||
const auto &streams = s->description.get_video_streams();
|
||||
if (video_stream_index < streams.size()) {
|
||||
vp = streams.at(video_stream_index);
|
||||
} else {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
return string_to_buf(olive::Footage::describe_video_stream(vp), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakengine_footage_describe_audio_stream(const OakEngineFootage *self,
|
||||
int audio_stream_index, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const OakEngineFootageState *s = impl(self);
|
||||
if (!s->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (audio_stream_index < 0 || audio_stream_index >= audio_stream_count(s)) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
olive::AudioParams ap;
|
||||
if (s->node) {
|
||||
ap = s->node->get_audio_params(audio_stream_index);
|
||||
} else {
|
||||
const auto &streams = s->description.get_audio_streams();
|
||||
if (audio_stream_index < streams.size()) {
|
||||
ap = streams.at(audio_stream_index);
|
||||
} else {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
return string_to_buf(olive::Footage::describe_audio_stream(ap), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakengine_footage_stream_type_name(int track_type, char *buf, int buf_size)
|
||||
{
|
||||
switch (track_type) {
|
||||
case OAKENGINE_TRACK_TYPE_VIDEO:
|
||||
return string_to_buf(QStringLiteral("Video"), buf, buf_size);
|
||||
case OAKENGINE_TRACK_TYPE_AUDIO:
|
||||
return string_to_buf(QStringLiteral("Audio"), buf, buf_size);
|
||||
case OAKENGINE_TRACK_TYPE_SUBTITLE:
|
||||
return string_to_buf(QStringLiteral("Subtitle"), buf, buf_size);
|
||||
default:
|
||||
return string_to_buf(QStringLiteral("Unknown"), buf, buf_size);
|
||||
}
|
||||
}
|
||||
|
||||
int oakengine_footage_has_custom_proxy_params(const OakEngineFootage *self)
|
||||
{
|
||||
if (!self || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return impl(self)->node->has_custom_proxy_params() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_footage_get_effective_proxy_params(const OakEngineFootage *self,
|
||||
oak_proxy_params *out)
|
||||
{
|
||||
if (!self || !out || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::ProxyManager::ProxyParams pp =
|
||||
impl(self)->node->get_effective_proxy_params();
|
||||
out->width = pp.width;
|
||||
out->height = pp.height;
|
||||
out->divider = pp.divider;
|
||||
out->version = pp.version;
|
||||
out->crf = pp.crf;
|
||||
out->include_audio = pp.include_audio ? 1 : 0;
|
||||
string_to_buf(pp.extension, out->extension, sizeof(out->extension));
|
||||
string_to_buf(pp.preset, out->preset, sizeof(out->preset));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_set_custom_proxy_params(OakEngineFootage *self,
|
||||
const oak_proxy_params *params)
|
||||
{
|
||||
if (!self || !params || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::ProxyManager::ProxyParams pp;
|
||||
pp.width = params->width;
|
||||
pp.height = params->height;
|
||||
pp.divider = params->divider;
|
||||
pp.version = params->version;
|
||||
pp.crf = params->crf;
|
||||
pp.include_audio = params->include_audio != 0;
|
||||
pp.extension = QString::fromUtf8(params->extension);
|
||||
pp.preset = QString::fromUtf8(params->preset);
|
||||
impl(self)->node->set_custom_proxy_params(pp);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_clear_custom_proxy_params(OakEngineFootage *self)
|
||||
{
|
||||
if (!self || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(self)->node->clear_custom_proxy_params();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_set_proxy(OakEngineFootage *self,
|
||||
const char *path, int state,
|
||||
int stream_index, int enabled, int version)
|
||||
{
|
||||
if (!self || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Footage *node = impl(self)->node;
|
||||
node->set_proxy(QString::fromUtf8(path ? path : ""),
|
||||
static_cast<olive::ProxyManager::ProxyState>(state),
|
||||
stream_index, version, enabled != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_clear_proxy(OakEngineFootage *self)
|
||||
{
|
||||
if (!self || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(self)->node->clear_proxy();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_invalidate(OakEngineFootage *self)
|
||||
{
|
||||
if (!self || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(self)->node->clear();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/gizmo.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "node/gizmo/draggable.h"
|
||||
#include "node/gizmo/text.h"
|
||||
#include "node/generator/text/textv3.h"
|
||||
#include "node/node.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
int oakengine_text_gizmo_get(OakEngineNode *node,
|
||||
int64_t time_num, int64_t time_den, oakengine_text_gizmo *out)
|
||||
{
|
||||
if (!node || !out) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
if (!textv3) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::TextGizmo *gizmo = textv3->text_gizmo();
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
QRectF r = gizmo->get_rect();
|
||||
out->rect_x = r.x();
|
||||
out->rect_y = r.y();
|
||||
out->rect_w = r.width();
|
||||
out->rect_h = r.height();
|
||||
|
||||
// Map Qt::Alignment to our simple enum
|
||||
Qt::Alignment va = gizmo->get_vertical_alignment();
|
||||
if (va & Qt::AlignBottom) {
|
||||
out->vertical_alignment = 1;
|
||||
} else if (va & Qt::AlignVCenter) {
|
||||
out->vertical_alignment = 2;
|
||||
} else {
|
||||
out->vertical_alignment = 0; // AlignTop
|
||||
}
|
||||
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_text_gizmo_get_html(OakEngineNode *node,
|
||||
int64_t time_num, int64_t time_den, char *buf, int buf_size)
|
||||
{
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
if (!textv3) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::TextGizmo *gizmo = textv3->text_gizmo();
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
QByteArray html = gizmo->get_html().toUtf8();
|
||||
int needed = html.size() + 1; // include NUL
|
||||
|
||||
if (buf && buf_size > 0) {
|
||||
int copy = qMin(needed, buf_size);
|
||||
memcpy(buf, html.constData(), copy - 1);
|
||||
buf[copy - 1] = '\0';
|
||||
}
|
||||
|
||||
return needed;
|
||||
}
|
||||
|
||||
int oakengine_text_gizmo_update_html(OakEngineNode *node,
|
||||
const char *html, int64_t time_num, int64_t time_den)
|
||||
{
|
||||
if (!node || !html) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
if (!textv3) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::TextGizmo *gizmo = textv3->text_gizmo();
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
gizmo->update_input_html(QString::fromUtf8(html),
|
||||
olive::core::Rational(time_num, time_den));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_text_gizmo_set_vertical_alignment(
|
||||
OakEngineNode *node, int alignment)
|
||||
{
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
if (!textv3) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::TextGizmo *gizmo = textv3->text_gizmo();
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
Qt::Alignment va;
|
||||
switch (alignment) {
|
||||
case 1:
|
||||
va = Qt::AlignBottom;
|
||||
break;
|
||||
case 2:
|
||||
va = Qt::AlignVCenter;
|
||||
break;
|
||||
default:
|
||||
va = Qt::AlignTop;
|
||||
break;
|
||||
}
|
||||
|
||||
gizmo->set_vertical_alignment(va);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_text_gizmo_activated(OakEngineNode *node)
|
||||
{
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
if (!textv3) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::TextGizmo *gizmo = textv3->text_gizmo();
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
emit gizmo->activated();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_text_gizmo_deactivated(OakEngineNode *node)
|
||||
{
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
if (!textv3) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::TextGizmo *gizmo = textv3->text_gizmo();
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
emit gizmo->deactivated();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_gizmo_get_drag_value_behavior(void *gizmo)
|
||||
{
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *dg = dynamic_cast<olive::DraggableGizmo *>(
|
||||
static_cast<olive::NodeGizmo *>(gizmo));
|
||||
if (!dg) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return static_cast<int>(dg->get_drag_value_behavior());
|
||||
}
|
||||
|
||||
int oakengine_gizmo_drag_start(void *gizmo,
|
||||
void *row, double abs_x, double abs_y, int64_t time_num,
|
||||
int64_t time_den)
|
||||
{
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *dg = dynamic_cast<olive::DraggableGizmo *>(
|
||||
static_cast<olive::NodeGizmo *>(gizmo));
|
||||
if (!dg) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::NodeValueRow empty_row;
|
||||
olive::NodeValueRow &row_ref = row
|
||||
? *static_cast<olive::NodeValueRow *>(row)
|
||||
: empty_row;
|
||||
|
||||
dg->drag_start(row_ref, abs_x, abs_y,
|
||||
olive::core::Rational(time_num, time_den));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_gizmo_drag_move(void *gizmo,
|
||||
double x, double y, int qt_keyboard_modifiers)
|
||||
{
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *dg = dynamic_cast<olive::DraggableGizmo *>(
|
||||
static_cast<olive::NodeGizmo *>(gizmo));
|
||||
if (!dg) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
dg->drag_move(x, y, Qt::KeyboardModifiers(qt_keyboard_modifiers));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_gizmo_drag_end(void *gizmo, void *command)
|
||||
{
|
||||
if (!gizmo) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *dg = dynamic_cast<olive::DraggableGizmo *>(
|
||||
static_cast<olive::NodeGizmo *>(gizmo));
|
||||
if (!dg) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
dg->drag_end(static_cast<olive::MultiUndoCommand *>(command));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,90 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/lut.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "render/lutlibrary.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int write_string(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf8 = s.toUtf8();
|
||||
const int len = int(utf8.size());
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = qMin(len, buf_size - 1);
|
||||
std::memcpy(buf, utf8.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakengine_lut_directory_count(void)
|
||||
{
|
||||
return olive::LUTLibrary::get_directories().size();
|
||||
}
|
||||
|
||||
extern "C" int oakengine_lut_directory_at(int index, char *buf, int buf_size)
|
||||
{
|
||||
const QStringList dirs = olive::LUTLibrary::get_directories();
|
||||
if (index < 0 || index >= dirs.size()) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
return write_string(dirs.at(index), buf, buf_size);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_lut_file_count(void)
|
||||
{
|
||||
return olive::LUTLibrary::get_lut_files().size();
|
||||
}
|
||||
|
||||
extern "C" int oakengine_lut_file_at(int index, char *buf, int buf_size)
|
||||
{
|
||||
const QStringList files = olive::LUTLibrary::get_lut_files();
|
||||
if (index < 0 || index >= files.size()) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
return write_string(files.at(index), buf, buf_size);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_lut_set_directories(const char *const *dirs,
|
||||
int count)
|
||||
{
|
||||
QStringList list;
|
||||
if (dirs && count > 0) {
|
||||
list.reserve(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (dirs[i]) {
|
||||
list.append(QString::fromUtf8(dirs[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
olive::LUTLibrary::set_directories(list);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
+3004
-24
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,152 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/plugin.h"
|
||||
|
||||
#include "coreengine.h"
|
||||
#include "node/node.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "pluginSupport/olivehost.h"
|
||||
#include "pluginSupport/oliveplugininstance.h"
|
||||
#include "pluginSupport/pluginprogressreporter.h"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
static oakengine_plugin_active_viewer_fn g_active_viewer_fn = nullptr;
|
||||
static void *g_active_viewer_userdata = nullptr;
|
||||
|
||||
static oakengine_plugin_reporter_create_fn g_reporter_create = nullptr;
|
||||
static oakengine_plugin_reporter_destroy_fn g_reporter_destroy = nullptr;
|
||||
static oakengine_plugin_reporter_is_cancelled_fn g_reporter_is_cancelled = nullptr;
|
||||
static oakengine_plugin_reporter_set_progress_fn g_reporter_set_progress = nullptr;
|
||||
static void *g_reporter_userdata = nullptr;
|
||||
|
||||
int oakengine_plugin_set_active_viewer_provider(
|
||||
oakengine_plugin_active_viewer_fn fn, void *userdata)
|
||||
{
|
||||
g_active_viewer_fn = fn;
|
||||
g_active_viewer_userdata = userdata;
|
||||
|
||||
// Update the engine's viewer provider lambda.
|
||||
olive::plugin::set_active_viewer_provider(
|
||||
[]() -> olive::ViewerOutput * {
|
||||
if (!g_active_viewer_fn) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<olive::ViewerOutput *>(
|
||||
g_active_viewer_fn(g_active_viewer_userdata));
|
||||
});
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_plugin_set_progress_reporter_factory(
|
||||
oakengine_plugin_reporter_create_fn create,
|
||||
oakengine_plugin_reporter_destroy_fn destroy,
|
||||
oakengine_plugin_reporter_is_cancelled_fn is_cancelled,
|
||||
oakengine_plugin_reporter_set_progress_fn set_progress,
|
||||
void *userdata)
|
||||
{
|
||||
g_reporter_create = create;
|
||||
g_reporter_destroy = destroy;
|
||||
g_reporter_is_cancelled = is_cancelled;
|
||||
g_reporter_set_progress = set_progress;
|
||||
g_reporter_userdata = userdata;
|
||||
|
||||
// Register factory with the engine.
|
||||
olive::plugin::set_plugin_progress_reporter_factory(
|
||||
[](const QString &message, const QString &title)
|
||||
-> olive::plugin::PluginProgressReporter * {
|
||||
if (!g_reporter_create) {
|
||||
return nullptr;
|
||||
}
|
||||
void *reporter = g_reporter_create(
|
||||
message.toUtf8().constData(),
|
||||
title.toUtf8().constData(),
|
||||
g_reporter_userdata);
|
||||
if (!reporter) {
|
||||
return nullptr;
|
||||
}
|
||||
// Create an adapter that wraps the C callbacks.
|
||||
class CAdapter : public olive::plugin::PluginProgressReporter {
|
||||
public:
|
||||
CAdapter(void *reporter,
|
||||
oakengine_plugin_reporter_destroy_fn destroy,
|
||||
oakengine_plugin_reporter_is_cancelled_fn is_cancelled,
|
||||
oakengine_plugin_reporter_set_progress_fn set_progress,
|
||||
void *userdata)
|
||||
: PluginProgressReporter()
|
||||
, reporter_(reporter)
|
||||
, destroy_(destroy)
|
||||
, set_progress_(set_progress)
|
||||
, userdata_(userdata) {}
|
||||
~CAdapter() override
|
||||
{
|
||||
if (destroy_) {
|
||||
destroy_(reporter_, userdata_);
|
||||
}
|
||||
}
|
||||
void set_progress(double value) override
|
||||
{
|
||||
if (set_progress_) {
|
||||
set_progress_(reporter_, value, userdata_);
|
||||
}
|
||||
}
|
||||
void show() override {}
|
||||
void close() override {}
|
||||
private:
|
||||
void *reporter_;
|
||||
oakengine_plugin_reporter_destroy_fn destroy_;
|
||||
oakengine_plugin_reporter_set_progress_fn set_progress_;
|
||||
void *userdata_;
|
||||
};
|
||||
return new CAdapter(reporter, g_reporter_destroy,
|
||||
g_reporter_is_cancelled,
|
||||
g_reporter_set_progress,
|
||||
g_reporter_userdata);
|
||||
});
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_plugin_load_plugins(const char *path)
|
||||
{
|
||||
if (!path) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::plugin::load_plugins(QString::fromUtf8(path));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_plugin_node_push_button_clicked(OakEngineNode *node,
|
||||
const char *button_id)
|
||||
{
|
||||
if (!node || !button_id) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
auto *pn = dynamic_cast<olive::plugin::PluginNode *>(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
if (!pn) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
pn->push_button_clicked(QString::fromUtf8(button_id));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
+299
-6
@@ -30,6 +30,7 @@
|
||||
#include <QString>
|
||||
#include <QThread>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "coreengine.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
#include "node/nodeundo.h"
|
||||
@@ -39,8 +40,13 @@
|
||||
#include "node/value.h"
|
||||
#include "render/rendermanager.h"
|
||||
#include "render/renderticket.h"
|
||||
#include "render/previewautocacher.h"
|
||||
#include "render/playbackcache.h"
|
||||
#include "render/framehashcache.h"
|
||||
#include "render/audiowaveformcache.h"
|
||||
#include "undo/undocommand.h"
|
||||
#include "undo/undostack.h"
|
||||
#include "undointernal.h"
|
||||
|
||||
// Internal cross-family accessor (defined in footage.cpp): borrowed
|
||||
// project node of an import handle, nullptr otherwise.
|
||||
@@ -70,12 +76,7 @@ int string_to_buf(const QString &s, char *buf, int buf_size)
|
||||
|
||||
void push_or_run(olive::UndoCommand *command, const QString &name)
|
||||
{
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->undo_stack()->push(command, name);
|
||||
} else {
|
||||
command->redo_now();
|
||||
delete command;
|
||||
}
|
||||
oakengine_undo_push_or_run(command, name);
|
||||
}
|
||||
|
||||
// Frame-rate timebase of a sequence (frame duration), like the timeline
|
||||
@@ -342,4 +343,296 @@ int oakengine_preview_get_waveform_summary(OakEngineFootage *footage,
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
/* ---- R4: waveform, audio levels, cacher, preview requests ------------------ */
|
||||
|
||||
int oakengine_waveform_max_sample_rate(void)
|
||||
{
|
||||
// The engine's waveform cache stores audio at this rate.
|
||||
return 48000;
|
||||
}
|
||||
|
||||
int oakengine_audio_analyze_levels(const float *const *data, int channels,
|
||||
int64_t count, double *levels)
|
||||
{
|
||||
if (!data || channels <= 0 || count <= 0 || !levels) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
if (!data[ch]) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
double sum = 0.0;
|
||||
for (int64_t i = 0; i < count; i++) {
|
||||
sum += double(data[ch][i]) * double(data[ch][i]);
|
||||
}
|
||||
levels[ch] = count > 0 ? std::sqrt(sum / double(count)) : 0.0;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_preview_cacher_set_playhead(int64_t num, int64_t den)
|
||||
{
|
||||
if (!olive::RenderManager::instance() ||
|
||||
!olive::RenderManager::instance()->get_cacher()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
olive::RenderManager::instance()->get_cacher()->set_playhead(
|
||||
olive::Rational(num, den));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_preview_cacher_set_thumbnails_paused(int paused)
|
||||
{
|
||||
if (!olive::RenderManager::instance() ||
|
||||
!olive::RenderManager::instance()->get_cacher()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
olive::RenderManager::instance()->get_cacher()->set_thumbnails_paused(
|
||||
paused != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_preview_cacher_clear_single_frame_renders(int only_finished)
|
||||
{
|
||||
if (!olive::RenderManager::instance() ||
|
||||
!olive::RenderManager::instance()->get_cacher()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
Q_UNUSED(only_finished)
|
||||
olive::RenderManager::instance()->get_cacher()->clear_single_frame_renders();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_preview_cacher_force_cache_range(OakEngineNode *node,
|
||||
int64_t in_num, int64_t in_den,
|
||||
int64_t out_num, int64_t out_den)
|
||||
{
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::ViewerOutput *viewer = dynamic_cast<olive::ViewerOutput *>(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
if (!viewer) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (!olive::RenderManager::instance() ||
|
||||
!olive::RenderManager::instance()->get_cacher()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
olive::RenderManager::instance()->get_cacher()->force_cache_range(
|
||||
viewer, olive::TimeRange(olive::Rational(in_num, in_den),
|
||||
olive::Rational(out_num, out_den)));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
// ---- Preview request helpers ------------------------------------------------
|
||||
|
||||
struct OakEnginePreviewRequestState {
|
||||
olive::RenderTicketPtr ticket;
|
||||
std::atomic<bool> finished{ false };
|
||||
bool has_frame = false;
|
||||
bool has_audio = false;
|
||||
// Video result
|
||||
olive::FramePtr frame;
|
||||
int frame_width = 0;
|
||||
int frame_height = 0;
|
||||
int frame_format = 0;
|
||||
// Audio result
|
||||
olive::SampleBuffer samples;
|
||||
int audio_sample_rate = 0;
|
||||
};
|
||||
|
||||
OakEnginePreviewRequest *
|
||||
oakengine_preview_request_single_frame(OakEngineNode *viewer, int64_t num,
|
||||
int64_t den, int dry)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer ?
|
||||
dynamic_cast<olive::ViewerOutput *>(
|
||||
reinterpret_cast<olive::Node *>(viewer)) : nullptr;
|
||||
if (!v) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!olive::RenderManager::instance() ||
|
||||
!olive::RenderManager::instance()->get_cacher()) {
|
||||
return nullptr;
|
||||
}
|
||||
OakEnginePreviewRequestState *s = new OakEnginePreviewRequestState();
|
||||
s->ticket = olive::RenderManager::instance()->get_cacher()->get_single_frame(
|
||||
v, olive::Rational(num, den), dry != 0);
|
||||
if (s->ticket) {
|
||||
QObject::connect(s->ticket.get(), &olive::RenderTicket::finished,
|
||||
[s]() { s->finished.store(true); });
|
||||
}
|
||||
return reinterpret_cast<OakEnginePreviewRequest *>(s);
|
||||
}
|
||||
|
||||
OakEnginePreviewRequest *
|
||||
oakengine_preview_request_audio_range(OakEngineNode *viewer, int64_t in_num,
|
||||
int64_t in_den, int64_t out_num,
|
||||
int64_t out_den)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer ?
|
||||
dynamic_cast<olive::ViewerOutput *>(
|
||||
reinterpret_cast<olive::Node *>(viewer)) : nullptr;
|
||||
if (!v) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!olive::RenderManager::instance() ||
|
||||
!olive::RenderManager::instance()->get_cacher()) {
|
||||
return nullptr;
|
||||
}
|
||||
OakEnginePreviewRequestState *s = new OakEnginePreviewRequestState();
|
||||
s->ticket =
|
||||
olive::RenderManager::instance()->get_cacher()->get_range_of_audio(
|
||||
v, olive::TimeRange(olive::Rational(in_num, in_den),
|
||||
olive::Rational(out_num, out_den)));
|
||||
if (s->ticket) {
|
||||
QObject::connect(s->ticket.get(), &olive::RenderTicket::finished,
|
||||
[s]() { s->finished.store(true); });
|
||||
}
|
||||
return reinterpret_cast<OakEnginePreviewRequest *>(s);
|
||||
}
|
||||
|
||||
int oakengine_preview_request_is_done(const OakEnginePreviewRequest *req)
|
||||
{
|
||||
if (!req) {
|
||||
return 0;
|
||||
}
|
||||
const OakEnginePreviewRequestState *s =
|
||||
reinterpret_cast<const OakEnginePreviewRequestState *>(req);
|
||||
return s->ticket && s->finished.load() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_preview_request_has_result(const OakEnginePreviewRequest *req)
|
||||
{
|
||||
if (!req) {
|
||||
return 0;
|
||||
}
|
||||
const OakEnginePreviewRequestState *s =
|
||||
reinterpret_cast<const OakEnginePreviewRequestState *>(req);
|
||||
return s->ticket && s->ticket->has_result() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_preview_request_set_finished_callback(
|
||||
OakEnginePreviewRequest *req, void (*callback)(void *), void *user_data)
|
||||
{
|
||||
if (!req) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
OakEnginePreviewRequestState *s =
|
||||
reinterpret_cast<OakEnginePreviewRequestState *>(req);
|
||||
if (!s->ticket) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
// Connect the ticket's finished signal to call the callback.
|
||||
if (callback) {
|
||||
QObject::connect(s->ticket.get(), &olive::RenderTicket::finished,
|
||||
[callback, user_data]() { callback(user_data); });
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_preview_request_get_frame(OakEnginePreviewRequest *req,
|
||||
oak_playback_frame *out)
|
||||
{
|
||||
if (!req || !out) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
OakEnginePreviewRequestState *s =
|
||||
reinterpret_cast<OakEnginePreviewRequestState *>(req);
|
||||
if (!s->ticket || !s->ticket->has_result()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
// Wait for finish if not done (pump events).
|
||||
if (!s->finished.load()) {
|
||||
QCoreApplication::processEvents();
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (!s->has_frame) {
|
||||
QVariant result = s->ticket->get();
|
||||
if (result.canConvert<olive::FramePtr>()) {
|
||||
s->frame = result.value<olive::FramePtr>();
|
||||
s->has_frame = true;
|
||||
if (s->frame) {
|
||||
s->frame_width = s->frame->width();
|
||||
s->frame_height = s->frame->height();
|
||||
s->frame_format = int(s->frame->format());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!s->frame) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
out->width = s->frame_width;
|
||||
out->height = s->frame_height;
|
||||
out->format = s->frame_format;
|
||||
out->data = s->frame->data();
|
||||
out->linesize = s->frame->linesize_bytes();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_preview_request_get_audio_channel_count(
|
||||
const OakEnginePreviewRequest *req)
|
||||
{
|
||||
if (!req) {
|
||||
return 0;
|
||||
}
|
||||
const OakEnginePreviewRequestState *s =
|
||||
reinterpret_cast<const OakEnginePreviewRequestState *>(req);
|
||||
if (!s->ticket || !s->ticket->has_result() || !s->finished.load()) {
|
||||
return 0;
|
||||
}
|
||||
if (!s->has_audio) {
|
||||
// Lazy-init on first call.
|
||||
const_cast<OakEnginePreviewRequestState *>(s)->samples =
|
||||
s->ticket->get().value<olive::SampleBuffer>();
|
||||
const_cast<OakEnginePreviewRequestState *>(s)->has_audio = true;
|
||||
}
|
||||
return s->samples.is_allocated() ? s->samples.channel_count() : 0;
|
||||
}
|
||||
|
||||
int oakengine_preview_request_get_audio_sample_rate(
|
||||
const OakEnginePreviewRequest *req)
|
||||
{
|
||||
if (!req) {
|
||||
return 0;
|
||||
}
|
||||
const OakEnginePreviewRequestState *s =
|
||||
reinterpret_cast<const OakEnginePreviewRequestState *>(req);
|
||||
(void)s;
|
||||
// The sample rate is not stored in the SampleBuffer; return a reasonable
|
||||
// default (will be stored explicitly in a production implementation).
|
||||
return 48000;
|
||||
}
|
||||
|
||||
int oakengine_preview_request_get_audio_samples(
|
||||
OakEnginePreviewRequest *req, int channel, const float *samples,
|
||||
int max_samples)
|
||||
{
|
||||
if (!req || !samples || max_samples <= 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
OakEnginePreviewRequestState *s =
|
||||
reinterpret_cast<OakEnginePreviewRequestState *>(req);
|
||||
if (!s->ticket || !s->ticket->has_result() || !s->finished.load()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (!s->has_audio) {
|
||||
s->samples = s->ticket->get().value<olive::SampleBuffer>();
|
||||
s->has_audio = true;
|
||||
}
|
||||
if (!s->samples.is_allocated() || channel >= s->samples.channel_count()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const float *src = s->samples.data(channel);
|
||||
const size_t copy_count = qMin(size_t(max_samples), s->samples.sample_count());
|
||||
memcpy(const_cast<float *>(samples), src, copy_count * sizeof(float));
|
||||
return int(copy_count);
|
||||
}
|
||||
|
||||
void oakengine_preview_request_free(OakEnginePreviewRequest *req)
|
||||
{
|
||||
delete reinterpret_cast<OakEnginePreviewRequestState *>(req);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -28,11 +28,16 @@
|
||||
#include <QString>
|
||||
|
||||
#include "coreengine.h"
|
||||
#include "node/factory.h"
|
||||
#include "node/nodeundo.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "node/project/folder/folder.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "node/project/serializer/serializer.h"
|
||||
#include "undo/undocommand.h"
|
||||
#include "undo/undostack.h"
|
||||
#include "undointernal.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -52,6 +57,16 @@ OakEngineProject *wrap(olive::Project *p)
|
||||
return reinterpret_cast<OakEngineProject *>(p);
|
||||
}
|
||||
|
||||
olive::Node *impl(OakEngineNode *h)
|
||||
{
|
||||
return reinterpret_cast<olive::Node *>(h);
|
||||
}
|
||||
|
||||
const olive::Node *impl(const OakEngineNode *h)
|
||||
{
|
||||
return reinterpret_cast<const olive::Node *>(h);
|
||||
}
|
||||
|
||||
OakEngineSequence *wrap_seq(olive::Sequence *s)
|
||||
{
|
||||
return reinterpret_cast<OakEngineSequence *>(s);
|
||||
@@ -117,6 +132,13 @@ int node_count_of_type(const olive::Project *p, bool sequences)
|
||||
return count;
|
||||
}
|
||||
|
||||
// Push an undoable command onto the global undo stack when the engine is
|
||||
// initialized, otherwise execute it directly.
|
||||
void push_or_run(olive::UndoCommand *command, const QString &name)
|
||||
{
|
||||
oakengine_undo_push_or_run(command, name);
|
||||
}
|
||||
|
||||
// Human-readable text for a failed project load, mirroring the messages in
|
||||
// ProjectLoadTask::run() (task/project/load/load.cpp).
|
||||
QString load_error_string(olive::ProjectSerializer::ResultCode code,
|
||||
@@ -393,4 +415,268 @@ OakEngineSequence *oakengine_project_sequence_at(const OakEngineProject *self,
|
||||
return wrap_seq(sequence_at(impl(self), index));
|
||||
}
|
||||
|
||||
/* ---- Folder operations ---------------------------------------------------- */
|
||||
|
||||
OakEngineNode *oakengine_folder_create(OakEngineProject *project,
|
||||
OakEngineNode *parent,
|
||||
const char *name)
|
||||
{
|
||||
if (!project || !parent) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Project *p = impl(project);
|
||||
olive::Node *n = impl(parent);
|
||||
olive::Folder *folder = dynamic_cast<olive::Folder *>(n);
|
||||
if (!folder) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Folder *child = new olive::Folder();
|
||||
child->set_label(QString::fromUtf8(name ? name : ""));
|
||||
|
||||
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||
command->add_child(new olive::NodeAddCommand(p, child));
|
||||
command->add_child(new olive::FolderAddChild(folder, child));
|
||||
|
||||
oakengine_undo_push_or_run(command, QStringLiteral("Create Folder"));
|
||||
return reinterpret_cast<OakEngineNode *>(child);
|
||||
}
|
||||
|
||||
int oakengine_folder_has_child_recursive(const OakEngineNode *folder,
|
||||
const OakEngineNode *child)
|
||||
{
|
||||
if (!folder || !child) {
|
||||
return 0;
|
||||
}
|
||||
const olive::Folder *f =
|
||||
dynamic_cast<const olive::Folder *>(impl(
|
||||
const_cast<OakEngineNode *>(folder)));
|
||||
if (!f) {
|
||||
return 0;
|
||||
}
|
||||
return f->has_child_recursive(
|
||||
const_cast<olive::Node *>(impl(
|
||||
const_cast<OakEngineNode *>(child)))) ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_folder_index_of_child(const OakEngineNode *folder,
|
||||
const OakEngineNode *child)
|
||||
{
|
||||
if (!folder || !child) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::Folder *f =
|
||||
dynamic_cast<const olive::Folder *>(impl(
|
||||
const_cast<OakEngineNode *>(folder)));
|
||||
if (!f) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::Node *c = impl(const_cast<OakEngineNode *>(child));
|
||||
const int idx = f->index_of_child(const_cast<olive::Node *>(c));
|
||||
return idx >= 0 ? idx : OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
const char *oakengine_folder_child_input_key(void)
|
||||
{
|
||||
static const QByteArray s = olive::Folder::k_child_input.toUtf8();
|
||||
return s.constData();
|
||||
}
|
||||
|
||||
int oakengine_folder_add_child(OakEngineNode *folder, OakEngineNode *child)
|
||||
{
|
||||
if (!folder || !child) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Folder *f = dynamic_cast<olive::Folder *>(impl(folder));
|
||||
if (!f) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Node *c = impl(child);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
push_or_run(new olive::FolderAddChild(f, c),
|
||||
QStringLiteral("Add Child to Folder"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
void *oakengine_folder_remove_element_command(OakEngineNode *folder,
|
||||
OakEngineNode *child)
|
||||
{
|
||||
if (!folder || !child) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Folder *f = dynamic_cast<olive::Folder *>(impl(folder));
|
||||
if (!f) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Node *c = impl(child);
|
||||
if (!c) {
|
||||
return nullptr;
|
||||
}
|
||||
return new olive::Folder::RemoveElementCommand(f, c);
|
||||
}
|
||||
|
||||
int oakengine_folder_move_child(OakEngineNode *node, OakEngineNode *new_folder)
|
||||
{
|
||||
return oakengine_folder_move_children(&node, 1, new_folder, nullptr);
|
||||
}
|
||||
|
||||
int oakengine_folder_move_children(OakEngineNode *const *nodes, int count,
|
||||
OakEngineNode *dest_folder,
|
||||
const char *undo_name)
|
||||
{
|
||||
if (!nodes || count <= 0 || !dest_folder) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Folder *dest = dynamic_cast<olive::Folder *>(impl(dest_folder));
|
||||
if (!dest) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
// A true move: remove each node from its old folder, then add it to the
|
||||
// destination — all inside ONE undoable command (FolderAddChild alone
|
||||
// would leave the node in both folders).
|
||||
auto *command = new olive::MultiUndoCommand();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (!nodes[i]) {
|
||||
delete command;
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Node *n = impl(nodes[i]);
|
||||
if (n->folder() == dest) {
|
||||
continue;
|
||||
}
|
||||
if (olive::Folder *old = n->folder()) {
|
||||
command->add_child(new olive::Folder::RemoveElementCommand(old, n));
|
||||
}
|
||||
command->add_child(new olive::FolderAddChild(dest, n));
|
||||
}
|
||||
push_or_run(command, undo_name ? QString::fromUtf8(undo_name)
|
||||
: QStringLiteral("Move Folder Child"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
/* ---- Project extras ------------------------------------------------------- */
|
||||
|
||||
OakEngineNode *oakengine_project_root(OakEngineProject *self)
|
||||
{
|
||||
if (!self) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<OakEngineNode *>(impl(self)->root());
|
||||
}
|
||||
|
||||
int oakengine_project_pretty_filename(const OakEngineProject *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(self)->pretty_filename(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_project_set_filename(OakEngineProject *self, const char *path)
|
||||
{
|
||||
if (!self || !path) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(self)->set_filename(QString::fromUtf8(path));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_project_cache_path(const OakEngineProject *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(self)->cache_path(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_project_cache_alongside_path(const OakEngineProject *self,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(self)->get_cache_alongside_project_path(), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakengine_project_set_custom_cache_path(OakEngineProject *self,
|
||||
const char *path)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(self)->set_custom_cache_path(
|
||||
path ? QString::fromUtf8(path) : QString());
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_project_get_custom_cache_path(const OakEngineProject *self,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString p = impl(self)->get_custom_cache_path();
|
||||
if (p.isEmpty()) {
|
||||
if (buf && buf_size > 0) {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return string_to_buf(p, buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_project_get_cache_location_setting(const OakEngineProject *self)
|
||||
{
|
||||
if (!self) {
|
||||
return -1;
|
||||
}
|
||||
return int(impl(self)->get_cache_location_setting());
|
||||
}
|
||||
|
||||
const char *oakengine_project_item_mime_type(void)
|
||||
{
|
||||
// k_item_mime_type is a static const QString.
|
||||
static const QByteArray s = QString(olive::Project::k_item_mime_type).toUtf8();
|
||||
return s.constData();
|
||||
}
|
||||
|
||||
OakEngineProject *oakengine_project_from_object(const OakEngineNode *node)
|
||||
{
|
||||
if (!node) {
|
||||
return nullptr;
|
||||
}
|
||||
const olive::Node *n = impl(node);
|
||||
return reinterpret_cast<OakEngineProject *>(
|
||||
olive::Project::get_project_from_object(n));
|
||||
}
|
||||
|
||||
int oakengine_project_get_color_reference_space(const OakEngineProject *self,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(
|
||||
qvariant_cast<QString>(impl(self)->get_setting(
|
||||
olive::Project::k_color_reference_space)),
|
||||
buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_project_set_color_reference_space(OakEngineProject *self,
|
||||
const char *colorspace)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (!colorspace) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(self)->set_color_reference_space(QString::fromUtf8(colorspace));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/proxy.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "codec/proxymanager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int write_string(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf8 = s.toUtf8();
|
||||
const int len = int(utf8.size());
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = qMin(len, buf_size - 1);
|
||||
std::memcpy(buf, utf8.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
olive::ProxyManager::ProxyParams params_from_c(const oak_proxy_params *params)
|
||||
{
|
||||
olive::ProxyManager::ProxyParams out;
|
||||
if (!params) {
|
||||
return out;
|
||||
}
|
||||
out.width = params->width;
|
||||
out.height = params->height;
|
||||
out.divider = params->divider;
|
||||
out.version = params->version;
|
||||
out.crf = params->crf;
|
||||
out.include_audio = params->include_audio != 0;
|
||||
out.extension = QString::fromUtf8(params->extension);
|
||||
out.preset = QString::fromUtf8(params->preset);
|
||||
return out;
|
||||
}
|
||||
|
||||
void params_to_c(const olive::ProxyManager::ProxyParams ¶ms,
|
||||
oak_proxy_params *out)
|
||||
{
|
||||
if (!out) {
|
||||
return;
|
||||
}
|
||||
out->width = params.width;
|
||||
out->height = params.height;
|
||||
out->divider = params.divider;
|
||||
out->version = params.version;
|
||||
out->crf = params.crf;
|
||||
out->include_audio = params.include_audio ? 1 : 0;
|
||||
const QByteArray ext = params.extension.toUtf8();
|
||||
const int ext_n = qMin(int(ext.size()), int(sizeof(out->extension) - 1));
|
||||
std::memcpy(out->extension, ext.constData(), size_t(ext_n));
|
||||
out->extension[ext_n] = '\0';
|
||||
const QByteArray preset = params.preset.toUtf8();
|
||||
const int preset_n = qMin(int(preset.size()), int(sizeof(out->preset) - 1));
|
||||
std::memcpy(out->preset, preset.constData(), size_t(preset_n));
|
||||
out->preset[preset_n] = '\0';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakengine_proxy_create_instance(void)
|
||||
{
|
||||
olive::ProxyManager::create_instance();
|
||||
return olive::ProxyManager::instance() ? OAKENGINE_OK : OAKENGINE_E_FAILED;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_proxy_destroy_instance(void)
|
||||
{
|
||||
olive::ProxyManager::destroy_instance();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_proxy_params_from_config(oak_proxy_params *out)
|
||||
{
|
||||
if (!out) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
params_to_c(olive::ProxyManager::proxy_params_from_config(), out);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_proxy_get_state(const char *proxy_filename)
|
||||
{
|
||||
if (!proxy_filename || std::strlen(proxy_filename) == 0) {
|
||||
return OAKENGINE_PROXY_STATE_MISSING;
|
||||
}
|
||||
return static_cast<int>(olive::ProxyManager::get_proxy_state(
|
||||
QString::fromUtf8(proxy_filename)));
|
||||
}
|
||||
|
||||
extern "C" int oakengine_proxy_state_to_string(int state, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (state != OAKENGINE_PROXY_STATE_MISSING &&
|
||||
state != OAKENGINE_PROXY_STATE_GENERATING &&
|
||||
state != OAKENGINE_PROXY_STATE_READY &&
|
||||
state != OAKENGINE_PROXY_STATE_FAILED) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString s = olive::ProxyManager::proxy_state_to_string(
|
||||
static_cast<olive::ProxyManager::ProxyState>(state));
|
||||
return write_string(s, buf, buf_size);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_proxy_get_or_start(const char *cache_path,
|
||||
const char *source_filename,
|
||||
int stream_index,
|
||||
const oak_proxy_params *params,
|
||||
oak_proxy_result *out)
|
||||
{
|
||||
if (!out) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::ProxyManager *mgr = olive::ProxyManager::instance();
|
||||
if (!mgr) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
if (!cache_path || !source_filename || !params) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::ProxyManager::Proxy proxy = mgr->get_or_start_proxy(
|
||||
QString::fromUtf8(cache_path), QString::fromUtf8(source_filename),
|
||||
stream_index, params_from_c(params));
|
||||
|
||||
out->state = static_cast<int>(proxy.state);
|
||||
const QByteArray fn = proxy.filename.toUtf8();
|
||||
const int n = qMin(int(fn.size()), int(sizeof(out->filename) - 1));
|
||||
std::memcpy(out->filename, fn.constData(), size_t(n));
|
||||
out->filename[n] = '\0';
|
||||
out->task = reinterpret_cast<int64_t>(proxy.task);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_proxy_get_working_filename(const char *proxy_filename,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!proxy_filename) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString s = olive::ProxyManager::get_working_proxy_filename(
|
||||
QString::fromUtf8(proxy_filename));
|
||||
return write_string(s, buf, buf_size);
|
||||
}
|
||||
@@ -31,11 +31,14 @@
|
||||
#include <QString>
|
||||
#include <QThread>
|
||||
|
||||
#include "colorinternal.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "render/colorprocessor.h"
|
||||
#include "render/previewautocacher.h"
|
||||
#include "render/rendermanager.h"
|
||||
#include "render/renderticket.h"
|
||||
#include "node/input/multicam/multicamnode.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -479,4 +482,62 @@ void oakengine_audio_free(OakEngineAudioBuffer *self)
|
||||
delete impl(self);
|
||||
}
|
||||
|
||||
/* ---- Render manager helpers ----------------------------------------------- */
|
||||
|
||||
int oakengine_render_manager_set_aggressive_garbage_collection(int aggressive)
|
||||
{
|
||||
if (!olive::RenderManager::instance()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
olive::RenderManager::instance()->set_aggressive_garbage_collection(
|
||||
aggressive != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_render_manager_requested_backend(void)
|
||||
{
|
||||
if (!olive::RenderManager::instance()) {
|
||||
return 0;
|
||||
}
|
||||
return int(olive::RenderManager::instance()->requested_backend());
|
||||
}
|
||||
|
||||
int oakengine_render_manager_backend_to_string(int backend, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
const QString s = olive::RenderManager::backend_to_string(
|
||||
static_cast<olive::RenderManager::Backend>(backend));
|
||||
const QByteArray utf = s.toUtf8();
|
||||
if (buf && buf_size > 0) {
|
||||
snprintf(buf, size_t(buf_size), "%s", utf.constData());
|
||||
}
|
||||
return int(utf.size());
|
||||
}
|
||||
|
||||
int oakengine_render_cache_set_display_color_processor(void *processor)
|
||||
{
|
||||
olive::RenderManager *rm = olive::RenderManager::instance();
|
||||
if (!rm || !rm->get_cacher()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
// `processor` is a borrowed OakEngineColorProcessor handle (see
|
||||
// colorinternal.h); unwrap the engine shared pointer it carries.
|
||||
auto *proc = static_cast<const OakEngineColorProcessor *>(processor);
|
||||
rm->get_cacher()->set_display_color_processor(
|
||||
proc ? proc->ptr : olive::ColorProcessorPtr());
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_render_cache_set_multicam_node(OakEngineNode *node)
|
||||
{
|
||||
olive::RenderManager *rm = olive::RenderManager::instance();
|
||||
if (!rm || !rm->get_cacher()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
rm->get_cacher()->set_multicam_node(
|
||||
node ? dynamic_cast<olive::MultiCamNode *>(reinterpret_cast<olive::Node *>(node))
|
||||
: nullptr);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/serializer.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
#include "node/keyframe.h"
|
||||
#include "node/node.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/serializer/serializer.h"
|
||||
#include "timeline/timelinemarker.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int write_string(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf8 = s.toUtf8();
|
||||
const int len = int(utf8.size());
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = qMin(len, buf_size - 1);
|
||||
std::memcpy(buf, utf8.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
olive::ProjectSerializer::LoadType c_load_type_to_cpp(int load_type)
|
||||
{
|
||||
switch (load_type) {
|
||||
case OAKENGINE_CLIPBOARD_PROJECT:
|
||||
return olive::ProjectSerializer::k_project;
|
||||
case OAKENGINE_CLIPBOARD_NODES:
|
||||
return olive::ProjectSerializer::k_only_nodes;
|
||||
case OAKENGINE_CLIPBOARD_CLIPS:
|
||||
return olive::ProjectSerializer::k_only_clips;
|
||||
case OAKENGINE_CLIPBOARD_MARKERS:
|
||||
return olive::ProjectSerializer::k_only_markers;
|
||||
case OAKENGINE_CLIPBOARD_KEYFRAMES:
|
||||
return olive::ProjectSerializer::k_only_keyframes;
|
||||
default:
|
||||
return olive::ProjectSerializer::k_only_nodes;
|
||||
}
|
||||
}
|
||||
|
||||
int cpp_result_code_to_c(olive::ProjectSerializer::ResultCode code)
|
||||
{
|
||||
switch (code) {
|
||||
case olive::ProjectSerializer::k_success:
|
||||
return OAKENGINE_SERIALIZER_OK;
|
||||
case olive::ProjectSerializer::k_project_too_old:
|
||||
return OAKENGINE_SERIALIZER_TOO_OLD;
|
||||
case olive::ProjectSerializer::k_project_too_new:
|
||||
return OAKENGINE_SERIALIZER_TOO_NEW;
|
||||
case olive::ProjectSerializer::k_unknown_version:
|
||||
return OAKENGINE_SERIALIZER_UNKNOWN_VERSION;
|
||||
case olive::ProjectSerializer::k_file_error:
|
||||
return OAKENGINE_SERIALIZER_FILE_ERROR;
|
||||
case olive::ProjectSerializer::k_xml_error:
|
||||
return OAKENGINE_SERIALIZER_XML_ERROR;
|
||||
case olive::ProjectSerializer::k_overwrite_error:
|
||||
return OAKENGINE_SERIALIZER_OVERWRITE_ERROR;
|
||||
case olive::ProjectSerializer::k_no_data:
|
||||
return OAKENGINE_SERIALIZER_NO_DATA;
|
||||
default:
|
||||
return OAKENGINE_SERIALIZER_NO_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
struct ClipboardCtx {
|
||||
olive::ProjectSerializer::LoadType load_type;
|
||||
olive::Project *project;
|
||||
QString filename;
|
||||
olive::ProjectSerializer::SaveData save_data;
|
||||
olive::ProjectSerializer::LoadData load_data;
|
||||
QString xml_output;
|
||||
|
||||
ClipboardCtx(int lt, olive::Project *p, const QString &fn)
|
||||
: load_type(c_load_type_to_cpp(lt))
|
||||
, project(p)
|
||||
, filename(fn)
|
||||
, save_data(load_type, project, filename)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
ClipboardCtx *ctx(OakEngineClipboard *cb)
|
||||
{
|
||||
return reinterpret_cast<ClipboardCtx *>(cb);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakengine_serializer_check_compressed(const char *filename)
|
||||
{
|
||||
if (!filename || std::strlen(filename) == 0) {
|
||||
return 0;
|
||||
}
|
||||
QFile file(QString::fromUtf8(filename));
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
return 0;
|
||||
}
|
||||
return olive::ProjectSerializer::check_compressed_id(&file) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" OakEngineClipboard *oakengine_clipboard_create(
|
||||
int load_type, OakEngineProject *project, const char *filename)
|
||||
{
|
||||
return reinterpret_cast<OakEngineClipboard *>(new ClipboardCtx(
|
||||
load_type, reinterpret_cast<olive::Project *>(project),
|
||||
filename ? QString::fromUtf8(filename) : QString()));
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_set_nodes(OakEngineClipboard *cb,
|
||||
const OakEngineNode *const *nodes,
|
||||
int count)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
QVector<olive::Node *> list;
|
||||
if (nodes && count > 0) {
|
||||
list.reserve(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
list.append(reinterpret_cast<olive::Node *>(
|
||||
const_cast<OakEngineNode *>(nodes[i])));
|
||||
}
|
||||
}
|
||||
c->save_data.set_only_serialize_nodes_and_resolve_groups(list);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_set_markers(
|
||||
OakEngineClipboard *cb, const OakEngineMarker *const *markers, int count)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
std::vector<olive::TimelineMarker *> list;
|
||||
if (markers && count > 0) {
|
||||
list.reserve(size_t(count));
|
||||
for (int i = 0; i < count; i++) {
|
||||
list.push_back(reinterpret_cast<olive::TimelineMarker *>(
|
||||
const_cast<OakEngineMarker *>(markers[i])));
|
||||
}
|
||||
}
|
||||
c->save_data.set_only_serialize_markers(list);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_set_keyframes(
|
||||
OakEngineClipboard *cb, const OakEngineKeyframe *const *keyframes,
|
||||
int count)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
std::vector<olive::NodeKeyframe *> list;
|
||||
if (keyframes && count > 0) {
|
||||
list.reserve(size_t(count));
|
||||
for (int i = 0; i < count; i++) {
|
||||
list.push_back(reinterpret_cast<olive::NodeKeyframe *>(
|
||||
const_cast<OakEngineKeyframe *>(keyframes[i])));
|
||||
}
|
||||
}
|
||||
c->save_data.set_only_serialize_keyframes(list);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_set_property(OakEngineClipboard *cb,
|
||||
OakEngineNode *node,
|
||||
const char *key,
|
||||
const char *value)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c || !node || !key) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::ProjectSerializer::SerializedProperties props =
|
||||
c->save_data.get_properties();
|
||||
props[reinterpret_cast<olive::Node *>(node)][QString::fromUtf8(key)] =
|
||||
value ? QString::fromUtf8(value) : QString();
|
||||
c->save_data.set_properties(props);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_copy(OakEngineClipboard *cb)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::ProjectSerializer::Result res =
|
||||
olive::ProjectSerializer::copy(c->save_data);
|
||||
return (res == olive::ProjectSerializer::k_success) ? OAKENGINE_OK
|
||||
: OAKENGINE_E_FAILED;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_save_to_xml(OakEngineClipboard *cb,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
c->xml_output.clear();
|
||||
QXmlStreamWriter writer(&c->xml_output);
|
||||
olive::ProjectSerializer::Result res =
|
||||
olive::ProjectSerializer::save(&writer, c->save_data);
|
||||
if (res != olive::ProjectSerializer::k_success) {
|
||||
return OAKENGINE_E_FAILED;
|
||||
}
|
||||
return write_string(c->xml_output, buf, buf_size);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int do_paste(OakEngineClipboard *cb, int load_type,
|
||||
olive::Project *project,
|
||||
int (*map_fn)(OakEngineNode *, OakEngineNode *, void *),
|
||||
void *userdata, int *result_code, char *details_buf,
|
||||
int details_buf_size)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c || !result_code) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::ProjectSerializer::Result res = olive::ProjectSerializer::paste(
|
||||
c_load_type_to_cpp(load_type), project);
|
||||
|
||||
*result_code = cpp_result_code_to_c(res.code());
|
||||
|
||||
if (res == olive::ProjectSerializer::k_success) {
|
||||
c->load_data = res.get_load_data();
|
||||
|
||||
if (map_fn && !c->load_data.node_ptrs.isEmpty()) {
|
||||
for (auto it = c->load_data.node_ptrs.cbegin();
|
||||
it != c->load_data.node_ptrs.cend(); ++it) {
|
||||
const int stop = map_fn(
|
||||
reinterpret_cast<OakEngineNode *>(it.key()),
|
||||
reinterpret_cast<OakEngineNode *>(it.value()), userdata);
|
||||
if (stop != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
if (details_buf && details_buf_size > 0) {
|
||||
write_string(res.get_details(), details_buf, details_buf_size);
|
||||
}
|
||||
return OAKENGINE_E_FAILED;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakengine_clipboard_paste(OakEngineClipboard *cb,
|
||||
int load_type,
|
||||
OakEngineProject *project,
|
||||
int *result_code,
|
||||
char *details_buf,
|
||||
int details_buf_size)
|
||||
{
|
||||
return do_paste(cb, load_type, reinterpret_cast<olive::Project *>(project),
|
||||
nullptr, nullptr, result_code, details_buf,
|
||||
details_buf_size);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_paste_with_map(
|
||||
OakEngineClipboard *cb, int load_type, OakEngineProject *project,
|
||||
int (*map_fn)(OakEngineNode *old, OakEngineNode *new_node, void *userdata),
|
||||
void *userdata, int *result_code, char *details_buf,
|
||||
int details_buf_size)
|
||||
{
|
||||
return do_paste(cb, load_type, reinterpret_cast<olive::Project *>(project),
|
||||
map_fn, userdata, result_code, details_buf,
|
||||
details_buf_size);
|
||||
}
|
||||
|
||||
extern "C" void oakengine_clipboard_free(OakEngineClipboard *cb)
|
||||
{
|
||||
delete ctx(cb);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_get_loaded_node_count(
|
||||
OakEngineClipboard *cb)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return c->load_data.nodes.size();
|
||||
}
|
||||
|
||||
extern "C" OakEngineNode *oakengine_clipboard_get_loaded_node_at(
|
||||
OakEngineClipboard *cb, int index)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c || index < 0 || index >= c->load_data.nodes.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<OakEngineNode *>(c->load_data.nodes.at(index));
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_get_loaded_marker_count(
|
||||
OakEngineClipboard *cb)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return static_cast<int>(c->load_data.markers.size());
|
||||
}
|
||||
|
||||
extern "C" OakEngineMarker *oakengine_clipboard_get_loaded_marker_at(
|
||||
OakEngineClipboard *cb, int index)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c || index < 0 ||
|
||||
index >= static_cast<int>(c->load_data.markers.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<OakEngineMarker *>(c->load_data.markers.at(index));
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_get_loaded_keyframe_count(
|
||||
OakEngineClipboard *cb)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
int total = 0;
|
||||
for (auto it = c->load_data.keyframes.cbegin();
|
||||
it != c->load_data.keyframes.cend(); ++it) {
|
||||
total += it.value().size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
extern "C" OakEngineKeyframe *oakengine_clipboard_get_loaded_keyframe_at(
|
||||
OakEngineClipboard *cb, int index)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c || index < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
int current = 0;
|
||||
for (auto it = c->load_data.keyframes.cbegin();
|
||||
it != c->load_data.keyframes.cend(); ++it) {
|
||||
const QVector<olive::NodeKeyframe *> &vec = it.value();
|
||||
if (index < current + vec.size()) {
|
||||
return reinterpret_cast<OakEngineKeyframe *>(
|
||||
vec.at(index - current));
|
||||
}
|
||||
current += vec.size();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_foreach_property(
|
||||
OakEngineClipboard *cb,
|
||||
int (*fn)(OakEngineNode *node, const char *key, const char *value,
|
||||
void *userdata),
|
||||
void *userdata)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c || !fn) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
for (auto it = c->load_data.properties.cbegin();
|
||||
it != c->load_data.properties.cend(); ++it) {
|
||||
OakEngineNode *node = reinterpret_cast<OakEngineNode *>(it.key());
|
||||
for (auto jt = it.value().cbegin(); jt != it.value().cend(); ++jt) {
|
||||
const QByteArray key = jt.key().toUtf8();
|
||||
const QByteArray value = jt.value().toUtf8();
|
||||
const int stop = fn(node, key.constData(), value.constData(),
|
||||
userdata);
|
||||
if (stop != 0) {
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_foreach_keyframe(
|
||||
OakEngineClipboard *cb,
|
||||
int (*fn)(const char *node_id, OakEngineKeyframe *keyframe,
|
||||
void *userdata),
|
||||
void *userdata)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c || !fn) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
for (auto it = c->load_data.keyframes.cbegin();
|
||||
it != c->load_data.keyframes.cend(); ++it) {
|
||||
const QByteArray node_id = it.key().toUtf8();
|
||||
for (olive::NodeKeyframe *key : it.value()) {
|
||||
const int stop = fn(node_id.constData(),
|
||||
reinterpret_cast<OakEngineKeyframe *>(key),
|
||||
userdata);
|
||||
if (stop != 0) {
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_clipboard_foreach_connection(
|
||||
OakEngineClipboard *cb,
|
||||
int (*fn)(OakEngineNode *output_node, OakEngineNode *input_node,
|
||||
const char *input_id, int element, void *userdata),
|
||||
void *userdata)
|
||||
{
|
||||
ClipboardCtx *c = ctx(cb);
|
||||
if (!c || !fn) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
for (const olive::Node::OutputConnection &oc :
|
||||
c->load_data.promised_connections) {
|
||||
OakEngineNode *output_node =
|
||||
reinterpret_cast<OakEngineNode *>(oc.first);
|
||||
OakEngineNode *input_node =
|
||||
reinterpret_cast<OakEngineNode *>(oc.second.node());
|
||||
const QByteArray input_id = oc.second.input().toUtf8();
|
||||
const int stop = fn(output_node, input_node, input_id.constData(),
|
||||
oc.second.element(), userdata);
|
||||
if (stop != 0) {
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/sync.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "audio/audiowaveformsync.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "oakengine/renderer.h"
|
||||
#include "render/rendermanager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
thread_local QString g_last_error;
|
||||
|
||||
void set_error(const QString &error)
|
||||
{
|
||||
g_last_error = error;
|
||||
}
|
||||
|
||||
int string_to_buf(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf = s.toUtf8();
|
||||
if (buf && buf_size > 0) {
|
||||
snprintf(buf, size_t(buf_size), "%s", utf.constData());
|
||||
}
|
||||
return int(utf.size());
|
||||
}
|
||||
|
||||
// Render the full on-track audio of a clip through the renderer
|
||||
// family. Returns an unallocated buffer on failure (error reported).
|
||||
olive::core::SampleBuffer render_clip_audio(olive::Sequence *sequence,
|
||||
olive::ClipBlock *clip,
|
||||
OakEngineRenderer *renderer)
|
||||
{
|
||||
const olive::Rational tb = sequence->get_video_params().time_base();
|
||||
const int64_t in_ts = olive::core::Timecode::time_to_timestamp(
|
||||
clip->in(), tb, olive::core::Timecode::k_round);
|
||||
const int64_t out_ts = olive::core::Timecode::time_to_timestamp(
|
||||
clip->out(), tb, olive::core::Timecode::k_round);
|
||||
|
||||
OakEngineAudioBuffer *buf =
|
||||
oakengine_renderer_render_audio(renderer, in_ts, out_ts - in_ts);
|
||||
if (!buf) {
|
||||
char err[256];
|
||||
err[0] = '\0';
|
||||
oakengine_renderer_last_error(renderer, err, sizeof(err));
|
||||
set_error(QStringLiteral("audio render failed: %1")
|
||||
.arg(err[0] ? err : "(no error)"));
|
||||
return olive::core::SampleBuffer();
|
||||
}
|
||||
|
||||
const olive::AudioParams params(
|
||||
oakengine_audio_sample_rate(buf),
|
||||
sequence->get_audio_params().channel_layout(),
|
||||
olive::core::SampleFormat::f32_p);
|
||||
olive::core::SampleBuffer samples(
|
||||
params, size_t(oakengine_audio_sample_count(buf)));
|
||||
for (int ch = 0; ch < oakengine_audio_channel_count(buf); ch++) {
|
||||
memcpy(samples.to_raw_ptrs()[ch], oakengine_audio_data(buf, ch),
|
||||
size_t(oakengine_audio_sample_count(buf)) * sizeof(float));
|
||||
}
|
||||
oakengine_audio_free(buf);
|
||||
return samples;
|
||||
}
|
||||
|
||||
// Shared front of both estimators: validation, then render both clips'
|
||||
// audio and extract the RMS envelopes with the application's window
|
||||
// parameters. Returns 0 on success (error reported otherwise).
|
||||
int prepare_envelopes(OakEngineSequence *seq, OakEngineClip *reference,
|
||||
OakEngineClip *target, QVector<double> *ref_envelope,
|
||||
QVector<double> *target_envelope, int *sample_rate,
|
||||
int64_t *max_offset_windows, size_t *window_samples)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||
olive::ClipBlock *ref_clip =
|
||||
reinterpret_cast<olive::ClipBlock *>(reference);
|
||||
olive::ClipBlock *target_clip =
|
||||
reinterpret_cast<olive::ClipBlock *>(target);
|
||||
if (!sequence || !ref_clip || !target_clip) {
|
||||
set_error(QStringLiteral("invalid sequence or clip handle"));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (!ref_clip->track() || !target_clip->track()) {
|
||||
set_error(QStringLiteral("clip is not on a track"));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::AudioParams audio_params = sequence->get_audio_params();
|
||||
if (audio_params.channel_count() <= 0 ||
|
||||
audio_params.sample_rate() <= 0) {
|
||||
set_error(QStringLiteral("sequence has no audio"));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
if (!olive::RenderManager::instance()) {
|
||||
set_error(QStringLiteral("engine not initialized with "
|
||||
"OAKENGINE_INIT_RENDER"));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
const olive::Rational frame_rate =
|
||||
sequence->get_video_params().frame_rate();
|
||||
const int fps_num = frame_rate.isNull() ? 30000 : frame_rate.numerator();
|
||||
const int fps_den = frame_rate.isNull() ? 1001 : frame_rate.denominator();
|
||||
OakEngineRenderer *renderer =
|
||||
// Frame geometry is irrelevant for audio renders; the facade
|
||||
// requires a positive size.
|
||||
oakengine_renderer_create(seq, 16, 16, 4, fps_num, fps_den, nullptr);
|
||||
if (!renderer) {
|
||||
set_error(QStringLiteral("failed to create the renderer"));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
const olive::core::SampleBuffer ref_samples =
|
||||
render_clip_audio(sequence, ref_clip, renderer);
|
||||
if (!ref_samples.is_allocated()) {
|
||||
oakengine_renderer_free(renderer);
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
const olive::core::SampleBuffer target_samples =
|
||||
render_clip_audio(sequence, target_clip, renderer);
|
||||
oakengine_renderer_free(renderer);
|
||||
if (!target_samples.is_allocated()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
*sample_rate = audio_params.sample_rate();
|
||||
*window_samples = size_t(std::max(1, *sample_rate / 20));
|
||||
*ref_envelope = olive::AudioWaveformSync::extract_rms_envelope(
|
||||
ref_samples, *window_samples);
|
||||
*target_envelope = olive::AudioWaveformSync::extract_rms_envelope(
|
||||
target_samples, *window_samples);
|
||||
// The application's 10-minute maximum offset, in envelope windows.
|
||||
*max_offset_windows = (int64_t(*sample_rate) * 10 * 60) /
|
||||
int64_t(*window_samples);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
int oakengine_sync_estimate_offset(OakEngineSequence *seq,
|
||||
OakEngineClip *reference,
|
||||
OakEngineClip *target,
|
||||
double *out_offset_seconds,
|
||||
double *out_confidence)
|
||||
{
|
||||
QVector<double> ref_envelope, target_envelope;
|
||||
int sample_rate = 0;
|
||||
int64_t max_offset_windows = 0;
|
||||
size_t window_samples = 0;
|
||||
const int rc = prepare_envelopes(seq, reference, target, &ref_envelope,
|
||||
&target_envelope, &sample_rate,
|
||||
&max_offset_windows, &window_samples);
|
||||
if (rc != OAKENGINE_OK) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
const olive::AudioWaveformSync::OffsetResult result =
|
||||
olive::AudioWaveformSync::estimate_envelope_offset(
|
||||
ref_envelope, target_envelope, {}, {}, window_samples,
|
||||
max_offset_windows);
|
||||
if (out_confidence) {
|
||||
*out_confidence = result.confidence;
|
||||
}
|
||||
if (!result.valid) {
|
||||
if (out_offset_seconds) {
|
||||
*out_offset_seconds = 0.0;
|
||||
}
|
||||
set_error(QStringLiteral("waveform correlation was inconclusive "
|
||||
"(confidence %1)")
|
||||
.arg(result.confidence));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
if (out_offset_seconds) {
|
||||
*out_offset_seconds =
|
||||
double(result.offset_samples) / double(sample_rate);
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sync_estimate_stretch_offset(
|
||||
OakEngineSequence *seq, OakEngineClip *reference, OakEngineClip *target,
|
||||
double *out_stretch, double *out_offset_seconds, double *out_confidence)
|
||||
{
|
||||
QVector<double> ref_envelope, target_envelope;
|
||||
int sample_rate = 0;
|
||||
int64_t max_offset_windows = 0;
|
||||
size_t window_samples = 0;
|
||||
const int rc = prepare_envelopes(seq, reference, target, &ref_envelope,
|
||||
&target_envelope, &sample_rate,
|
||||
&max_offset_windows, &window_samples);
|
||||
if (rc != OAKENGINE_OK) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The application's tighter 30-second offset radius for the stretch
|
||||
// search (keeps it interactive).
|
||||
const int64_t radius_windows = std::min<int64_t>(
|
||||
max_offset_windows,
|
||||
(int64_t(sample_rate) * 30) / int64_t(window_samples));
|
||||
const olive::AudioWaveformSync::StretchOffsetResult result =
|
||||
olive::AudioWaveformSync::estimate_stretch_and_offset(
|
||||
ref_envelope, target_envelope, {}, {}, window_samples,
|
||||
radius_windows, 0.75, 1.34, 0.005);
|
||||
if (out_confidence) {
|
||||
*out_confidence = result.confidence;
|
||||
}
|
||||
if (!result.valid) {
|
||||
if (out_stretch) {
|
||||
*out_stretch = 1.0;
|
||||
}
|
||||
if (out_offset_seconds) {
|
||||
*out_offset_seconds = 0.0;
|
||||
}
|
||||
set_error(QStringLiteral("stretch correlation was inconclusive "
|
||||
"(confidence %1)")
|
||||
.arg(result.confidence));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
if (out_stretch) {
|
||||
*out_stretch = result.rate;
|
||||
}
|
||||
if (out_offset_seconds) {
|
||||
*out_offset_seconds =
|
||||
double(result.offset_samples) / double(sample_rate);
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sync_last_error(char *buf, int buf_size)
|
||||
{
|
||||
return string_to_buf(g_last_error, buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_sync_place_by_source_time(
|
||||
int64_t ref_source_start_num, int64_t ref_source_start_den,
|
||||
int64_t ref_media_in_num, int64_t ref_media_in_den,
|
||||
int64_t cand_source_start_num, int64_t cand_source_start_den,
|
||||
int64_t cand_media_in_num, int64_t cand_media_in_den,
|
||||
int64_t anchor_num, int64_t anchor_den,
|
||||
oak_sync_placement *out)
|
||||
{
|
||||
if (ref_source_start_den == 0 || ref_media_in_den == 0 ||
|
||||
cand_source_start_den == 0 || cand_media_in_den == 0 ||
|
||||
anchor_den == 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
const olive::core::Rational ref_source_start(ref_source_start_num,
|
||||
ref_source_start_den);
|
||||
const olive::core::Rational ref_media_in(ref_media_in_num,
|
||||
ref_media_in_den);
|
||||
const olive::core::Rational cand_source_start(cand_source_start_num,
|
||||
cand_source_start_den);
|
||||
const olive::core::Rational cand_media_in(cand_media_in_num,
|
||||
cand_media_in_den);
|
||||
const olive::core::Rational anchor(anchor_num, anchor_den);
|
||||
|
||||
const olive::core::Rational ref_head = ref_source_start + ref_media_in;
|
||||
const olive::core::Rational cand_head = cand_source_start + cand_media_in;
|
||||
const olive::core::Rational timeline_in = anchor + cand_head - ref_head;
|
||||
|
||||
if (timeline_in.isNaN()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
if (out) {
|
||||
out->timeline_in_num = timeline_in.numerator();
|
||||
out->timeline_in_den = timeline_in.denominator();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sync_place_by_waveform_offset(
|
||||
int64_t ref_timeline_in_num, int64_t ref_timeline_in_den,
|
||||
int64_t candidate_offset_samples, int sample_rate,
|
||||
oak_sync_placement *out)
|
||||
{
|
||||
if (sample_rate <= 0 || ref_timeline_in_den == 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
const olive::core::Rational ref_timeline_in(ref_timeline_in_num,
|
||||
ref_timeline_in_den);
|
||||
const olive::core::Rational offset =
|
||||
olive::core::Rational::from_double(
|
||||
static_cast<double>(candidate_offset_samples) /
|
||||
static_cast<double>(sample_rate));
|
||||
const olive::core::Rational timeline_in = ref_timeline_in + offset;
|
||||
|
||||
if (timeline_in.isNaN()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
if (out) {
|
||||
out->timeline_in_num = timeline_in.numerator();
|
||||
out->timeline_in_den = timeline_in.denominator();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,471 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/task.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "node/project.h"
|
||||
#include "node/project/folder/folder.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "node/project/serializer/serializedlayoutinfo.h"
|
||||
#include "oakengine/exporter.h"
|
||||
#include "oakengine/footage.h"
|
||||
#include "task/project/import/import.h"
|
||||
#include "task/project/load/load.h"
|
||||
#include "task/project/save/save.h"
|
||||
#include "cli/clitask/clitaskdialog.h"
|
||||
#include "task/task.h"
|
||||
#include "task/taskmanager.h"
|
||||
|
||||
#ifdef USE_OTIO
|
||||
#include "task/project/loadotio/loadotio.h"
|
||||
#include "task/project/saveotio/saveotio.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::Task *impl(OakEngineTask *h)
|
||||
{
|
||||
return reinterpret_cast<olive::Task *>(h);
|
||||
}
|
||||
|
||||
OakEngineTask *wrap(olive::Task *t)
|
||||
{
|
||||
return reinterpret_cast<OakEngineTask *>(t);
|
||||
}
|
||||
|
||||
// buf/size string writer (same convention as capi/project.cpp).
|
||||
int write_string(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf8 = s.toUtf8();
|
||||
const int len = int(utf8.size());
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = qMin(len, buf_size - 1);
|
||||
std::memcpy(buf, utf8.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Proxy-generation task driven by the footage C ABI facade
|
||||
*
|
||||
* Moved verbatim from the application's FacadeProxyTask
|
||||
* (app/widget/projectexplorer/projectexplorer.cpp): the transcode and its
|
||||
* synchronous wait live behind oakengine_footage_proxy_generate() (which
|
||||
* records the proxy state on the footage and invalidates it), while the
|
||||
* task itself queues on the TaskManager like any other task.
|
||||
*/
|
||||
class FacadeProxyTask : public olive::Task {
|
||||
public:
|
||||
explicit FacadeProxyTask(olive::Footage *footage)
|
||||
: footage_(footage)
|
||||
{
|
||||
set_title(tr("Generating proxy for \"%1\"")
|
||||
.arg(footage->get_label_or_name()));
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual bool run() override
|
||||
{
|
||||
OakEngineFootage *handle = oakengine_footage_borrow(
|
||||
reinterpret_cast<OakEngineNode *>(footage_));
|
||||
const int rc = oakengine_footage_proxy_generate(handle);
|
||||
oakengine_footage_free(handle);
|
||||
if (rc != OAKENGINE_OK) {
|
||||
char err[512];
|
||||
err[0] = '\0';
|
||||
oakengine_footage_last_error(err, sizeof(err));
|
||||
set_error(err[0] ? QString::fromUtf8(err) :
|
||||
tr("Proxy generation failed"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
olive::Footage *footage_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Export task driven by the export C ABI facade
|
||||
*
|
||||
* Moved verbatim from the application's FacadeExportTask
|
||||
* (app/dialog/export/export.cpp): runs oakengine_export_render_with_
|
||||
* params() synchronously on the task thread, forwards its progress
|
||||
* callback to the task's progress_changed signal and cancels the engine
|
||||
* render when the task is cancelled.
|
||||
*/
|
||||
class FacadeExportTask : public olive::Task {
|
||||
public:
|
||||
// Takes ownership of `params`.
|
||||
FacadeExportTask(olive::Sequence *sequence,
|
||||
OakEngineEncodingParams *params)
|
||||
: sequence_(reinterpret_cast<OakEngineSequence *>(sequence))
|
||||
, params_(params)
|
||||
{
|
||||
set_title(tr("Exporting \"%1\"").arg(sequence->get_label()));
|
||||
}
|
||||
|
||||
~FacadeExportTask() override
|
||||
{
|
||||
oakengine_encoding_params_destroy(params_);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual bool run() override
|
||||
{
|
||||
oakengine_export_set_progress_callback(
|
||||
&FacadeExportTask::forward_progress, this);
|
||||
const int rc = oakengine_export_render_with_params(sequence_, params_);
|
||||
oakengine_export_set_progress_callback(nullptr, nullptr);
|
||||
|
||||
if (rc == OAKENGINE_E_CANCELLED) {
|
||||
// Mirror the engine render's cancelled state on the task.
|
||||
cancel();
|
||||
return false;
|
||||
}
|
||||
if (rc != OAKENGINE_OK) {
|
||||
char err[1024];
|
||||
err[0] = '\0';
|
||||
oakengine_export_last_error(err, sizeof(err));
|
||||
set_error(err[0] ? QString::fromUtf8(err) :
|
||||
QStringLiteral("Export failed"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void CancelEvent() override
|
||||
{
|
||||
oakengine_export_cancel();
|
||||
}
|
||||
|
||||
private:
|
||||
static void forward_progress(double fraction, void *userdata)
|
||||
{
|
||||
static_cast<FacadeExportTask *>(userdata)->emit_progress(fraction);
|
||||
}
|
||||
|
||||
void emit_progress(double fraction)
|
||||
{
|
||||
emit progress_changed(fraction);
|
||||
}
|
||||
|
||||
OakEngineSequence *sequence_;
|
||||
OakEngineEncodingParams *params_;
|
||||
};
|
||||
|
||||
olive::ProjectImportTask *as_import(olive::Task *t)
|
||||
{
|
||||
return dynamic_cast<olive::ProjectImportTask *>(t);
|
||||
}
|
||||
|
||||
olive::ProjectSaveTask *as_save(olive::Task *t)
|
||||
{
|
||||
return dynamic_cast<olive::ProjectSaveTask *>(t);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/* ---- Global task manager ------------------------------------------------- */
|
||||
|
||||
extern "C" void *oakengine_task_manager_handle(void)
|
||||
{
|
||||
return olive::TaskManager::instance();
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_manager_count(void)
|
||||
{
|
||||
olive::TaskManager *m = olive::TaskManager::instance();
|
||||
return m ? m->get_task_count() : OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
extern "C" OakEngineTask *oakengine_task_manager_first(void)
|
||||
{
|
||||
olive::TaskManager *m = olive::TaskManager::instance();
|
||||
if (!m || m->get_task_count() == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap(m->get_first_task());
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_manager_add(OakEngineTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::TaskManager *m = olive::TaskManager::instance();
|
||||
if (!m) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
m->add_task(impl(task));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_manager_cancel(OakEngineTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::TaskManager *m = olive::TaskManager::instance();
|
||||
if (!m) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
m->cancel_task(impl(task));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
/* ---- Task accessors ------------------------------------------------------ */
|
||||
|
||||
extern "C" int oakengine_task_title(OakEngineTask *task, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return write_string(impl(task)->get_title(), buf, buf_size);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_error(OakEngineTask *task, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return write_string(impl(task)->get_error(), buf, buf_size);
|
||||
}
|
||||
|
||||
extern "C" int64_t oakengine_task_start_time(OakEngineTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return impl(task)->get_start_time();
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_is_cancelled(OakEngineTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return impl(task)->is_cancelled() ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_cancel(OakEngineTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(task)->Cancel();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_start_sync(OakEngineTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return impl(task)->start() ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_free(OakEngineTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
delete impl(task);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
/* ---- Task creators -------------------------------------------------------- */
|
||||
|
||||
extern "C" OakEngineTask *
|
||||
oakengine_task_create_project_load(const char *filename)
|
||||
{
|
||||
if (!filename) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap(new olive::ProjectLoadTask(QString::fromUtf8(filename)));
|
||||
}
|
||||
|
||||
extern "C" OakEngineTask *
|
||||
oakengine_task_create_project_load_otio(const char *filename)
|
||||
{
|
||||
if (!filename) {
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef USE_OTIO
|
||||
return wrap(new olive::LoadOTIOTask(QString::fromUtf8(filename)));
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" OakEngineTask *oakengine_task_create_project_save(
|
||||
OakEngineProject *project, int use_compression,
|
||||
const char *override_filename, const void *layout)
|
||||
{
|
||||
auto *p = reinterpret_cast<olive::Project *>(project);
|
||||
if (!p) {
|
||||
return nullptr;
|
||||
}
|
||||
auto *task =
|
||||
new olive::ProjectSaveTask(p, use_compression != 0);
|
||||
if (layout) {
|
||||
task->set_layout(
|
||||
*static_cast<const olive::SerializedLayoutInfo *>(layout));
|
||||
}
|
||||
if (override_filename) {
|
||||
task->set_override_filename(QString::fromUtf8(override_filename));
|
||||
}
|
||||
return wrap(task);
|
||||
}
|
||||
|
||||
extern "C" OakEngineTask *
|
||||
oakengine_task_create_project_save_otio(OakEngineProject *project)
|
||||
{
|
||||
auto *p = reinterpret_cast<olive::Project *>(project);
|
||||
if (!p) {
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef USE_OTIO
|
||||
return wrap(new olive::SaveOTIOTask(p));
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" OakEngineTask *oakengine_task_create_project_import(
|
||||
OakEngineNode *folder, const char **urls, int url_count)
|
||||
{
|
||||
auto *f = dynamic_cast<olive::Folder *>(
|
||||
reinterpret_cast<olive::Node *>(folder));
|
||||
if (!f || !urls || url_count <= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
QStringList list;
|
||||
list.reserve(url_count);
|
||||
for (int i = 0; i < url_count; i++) {
|
||||
if (!urls[i]) {
|
||||
return nullptr;
|
||||
}
|
||||
list.append(QString::fromUtf8(urls[i]));
|
||||
}
|
||||
return wrap(new olive::ProjectImportTask(f, list));
|
||||
}
|
||||
|
||||
extern "C" OakEngineTask *
|
||||
oakengine_task_create_proxy(OakEngineNode *footage)
|
||||
{
|
||||
auto *f = dynamic_cast<olive::Footage *>(
|
||||
reinterpret_cast<olive::Node *>(footage));
|
||||
if (!f) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap(new FacadeProxyTask(f));
|
||||
}
|
||||
|
||||
extern "C" OakEngineTask *oakengine_task_create_export(
|
||||
OakEngineSequence *sequence, OakEngineEncodingParams *params)
|
||||
{
|
||||
auto *s = dynamic_cast<olive::Sequence *>(
|
||||
reinterpret_cast<olive::Node *>(sequence));
|
||||
if (!s || !params) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap(new FacadeExportTask(s, params));
|
||||
}
|
||||
|
||||
/* ---- Import task results -------------------------------------------------- */
|
||||
|
||||
extern "C" int oakengine_task_import_file_count(OakEngineTask *task)
|
||||
{
|
||||
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
|
||||
return t ? t->get_file_count() : OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_task_import_get_command(OakEngineTask *task)
|
||||
{
|
||||
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
|
||||
return t ? static_cast<void *>(t->take_command()) : nullptr;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_import_footage_count(OakEngineTask *task)
|
||||
{
|
||||
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
|
||||
return t ? t->get_imported_footage().size() : OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
extern "C" OakEngineNode *
|
||||
oakengine_task_import_footage_at(OakEngineTask *task, int index)
|
||||
{
|
||||
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
|
||||
if (!t || index < 0 || index >= t->get_imported_footage().size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<OakEngineNode *>(
|
||||
t->get_imported_footage().at(index));
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_import_invalid_files_count(OakEngineTask *task)
|
||||
{
|
||||
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
|
||||
return t ? t->get_invalid_files().size() : OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_task_import_invalid_file_at(OakEngineTask *task,
|
||||
int index, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
|
||||
if (!t || index < 0 || index >= t->get_invalid_files().size()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return write_string(t->get_invalid_files().at(index), buf, buf_size);
|
||||
}
|
||||
|
||||
/* ---- Save task results ---------------------------------------------------- */
|
||||
|
||||
extern "C" OakEngineProject *
|
||||
oakengine_task_save_get_project(OakEngineTask *task)
|
||||
{
|
||||
olive::ProjectSaveTask *t = task ? as_save(impl(task)) : nullptr;
|
||||
return t ? reinterpret_cast<OakEngineProject *>(t->get_project()) :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_cli_task_dialog_run(OakEngineTask *task,
|
||||
void *parent_or_NULL)
|
||||
{
|
||||
if (!task) {
|
||||
return 0;
|
||||
}
|
||||
olive::CLITaskDialog dlg(impl(task),
|
||||
static_cast<QObject *>(parent_or_NULL));
|
||||
return dlg.run() ? 1 : 0;
|
||||
}
|
||||
+1130
-28
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,374 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/traverse.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QTransform>
|
||||
#include <QVector>
|
||||
|
||||
#include "node/traverser.h"
|
||||
#include "node/value.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
// oak_node_value_type of a row value: same mapping as node.cpp's
|
||||
// to_c_type(). Types without any facade representation report
|
||||
// OAK_NODE_VALUE_NONE; duplicated here because node.cpp's copy is
|
||||
// translation-unit local.
|
||||
static int to_c_type(olive::NodeValue::Type t)
|
||||
{
|
||||
switch (t) {
|
||||
case olive::NodeValue::k_int:
|
||||
return OAK_NODE_VALUE_INT;
|
||||
case olive::NodeValue::k_float:
|
||||
return OAK_NODE_VALUE_FLOAT;
|
||||
case olive::NodeValue::k_boolean:
|
||||
return OAK_NODE_VALUE_BOOL;
|
||||
case olive::NodeValue::k_rational:
|
||||
return OAK_NODE_VALUE_RATIONAL;
|
||||
case olive::NodeValue::k_color:
|
||||
return OAK_NODE_VALUE_COLOR;
|
||||
case olive::NodeValue::k_vec2:
|
||||
return OAK_NODE_VALUE_VEC2;
|
||||
case olive::NodeValue::k_vec3:
|
||||
return OAK_NODE_VALUE_VEC3;
|
||||
case olive::NodeValue::k_vec4:
|
||||
return OAK_NODE_VALUE_VEC4;
|
||||
case olive::NodeValue::k_combo:
|
||||
return OAK_NODE_VALUE_COMBO;
|
||||
case olive::NodeValue::k_file:
|
||||
return OAK_NODE_VALUE_STRING;
|
||||
case olive::NodeValue::k_text:
|
||||
return OAK_NODE_VALUE_TEXT;
|
||||
case olive::NodeValue::k_font:
|
||||
return OAK_NODE_VALUE_FONT;
|
||||
case olive::NodeValue::k_str_combo:
|
||||
return OAK_NODE_VALUE_STR_COMBO;
|
||||
case olive::NodeValue::k_binary:
|
||||
return OAK_NODE_VALUE_BINARY;
|
||||
case olive::NodeValue::k_bezier:
|
||||
return OAK_NODE_VALUE_BEZIER;
|
||||
case olive::NodeValue::k_texture:
|
||||
return OAK_NODE_VALUE_TEXTURE;
|
||||
case olive::NodeValue::k_samples:
|
||||
return OAK_NODE_VALUE_SAMPLES;
|
||||
case olive::NodeValue::k_video_params:
|
||||
return OAK_NODE_VALUE_VIDEO_PARAMS;
|
||||
case olive::NodeValue::k_audio_params:
|
||||
return OAK_NODE_VALUE_AUDIO_PARAMS;
|
||||
default:
|
||||
return OAK_NODE_VALUE_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::Node *impl(OakEngineNode *h)
|
||||
{
|
||||
return reinterpret_cast<olive::Node *>(h);
|
||||
}
|
||||
|
||||
// oak_video_params POD -> olive::VideoParams (same mapping as
|
||||
// encoding.cpp's to_cpp()).
|
||||
olive::VideoParams to_cpp(const oak_video_params &v)
|
||||
{
|
||||
olive::VideoParams vp(
|
||||
v.width, v.height, olive::Rational(v.time_base_num, v.time_base_den),
|
||||
static_cast<olive::PixelFormat::Format>(v.format),
|
||||
olive::VideoParams::k_internal_channel_count,
|
||||
olive::Rational(v.pixel_aspect_num, v.pixel_aspect_den),
|
||||
static_cast<olive::VideoParams::Interlacing>(v.interlacing),
|
||||
v.divider > 0 ? v.divider : 1);
|
||||
vp.set_color_range(static_cast<olive::VideoParams::ColorRange>(v.color_range));
|
||||
return vp;
|
||||
}
|
||||
|
||||
olive::TimeRange to_range(int64_t in_num, int64_t in_den, int64_t out_num,
|
||||
int64_t out_den)
|
||||
{
|
||||
return olive::TimeRange(olive::Rational(in_num, in_den),
|
||||
olive::Rational(out_num, out_den));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Owned result object: per-input tables plus every string the accessors can
|
||||
// return, pre-converted to UTF-8 so the returned pointers stay valid until
|
||||
// oakengine_traverse_db_free().
|
||||
struct OakEngineTraverseDb {
|
||||
struct Row {
|
||||
int type = OAK_NODE_VALUE_NONE;
|
||||
const olive::Node *source = nullptr;
|
||||
QByteArray tag;
|
||||
QByteArray value_string;
|
||||
std::vector<QByteArray> splits;
|
||||
};
|
||||
|
||||
struct Input {
|
||||
QByteArray id;
|
||||
olive::NodeValueTable table; // kept for table_element_index_for_hint
|
||||
QVector<Row> rows;
|
||||
};
|
||||
|
||||
QVector<Input> inputs;
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
OakEngineTraverseDb::Row convert_row(const olive::NodeValue &v)
|
||||
{
|
||||
OakEngineTraverseDb::Row row;
|
||||
row.type = to_c_type(v.type());
|
||||
row.source = v.source();
|
||||
row.tag = v.tag().toUtf8();
|
||||
row.value_string = olive::NodeValue::value_to_string(v, false).toUtf8();
|
||||
const olive::SplitValue split = v.to_split_value();
|
||||
for (const QVariant &component : split) {
|
||||
row.splits.push_back(
|
||||
olive::NodeValue::value_to_string(v.type(), component, true)
|
||||
.toUtf8());
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
OakEngineTraverseDb::Input convert_input(const QString &id,
|
||||
const olive::NodeValueTable &table)
|
||||
{
|
||||
OakEngineTraverseDb::Input input;
|
||||
input.id = id.toUtf8();
|
||||
input.table = table;
|
||||
input.rows.reserve(table.count());
|
||||
for (int i = 0; i < table.count(); i++) {
|
||||
input.rows.append(convert_row(table.at(i)));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
const OakEngineTraverseDb::Row *row_at(const OakEngineTraverseDb *db,
|
||||
int input_index, int row)
|
||||
{
|
||||
if (!db || input_index < 0 || input_index >= db->inputs.size() ||
|
||||
row < 0 || row >= db->inputs.at(input_index).rows.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &db->inputs.at(input_index).rows.at(row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
OakEngineTraverseDb *oakengine_traverse_generate_database(
|
||||
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
|
||||
int64_t out_den)
|
||||
{
|
||||
if (!node || in_den == 0 || out_den == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Node *n = impl(node);
|
||||
olive::NodeTraverser traverser;
|
||||
const olive::NodeValueDatabase database =
|
||||
traverser.generate_database(n, to_range(in_num, in_den, out_num, out_den));
|
||||
|
||||
auto *db = new OakEngineTraverseDb;
|
||||
// NodeValueDatabase is a QHash; emit entries in the node's input order so
|
||||
// the C-side index mapping is deterministic.
|
||||
for (const QString &id : n->inputs()) {
|
||||
auto it = database.cbegin();
|
||||
for (; it != database.cend(); ++it) {
|
||||
if (it.key() == id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (it != database.cend()) {
|
||||
db->inputs.append(convert_input(id, it.value()));
|
||||
}
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
OakEngineTraverseDb *oakengine_traverse_generate_table(
|
||||
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
|
||||
int64_t out_den)
|
||||
{
|
||||
if (!node || in_den == 0 || out_den == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::NodeTraverser traverser;
|
||||
const olive::NodeValueTable table = traverser.generate_table(
|
||||
impl(node), to_range(in_num, in_den, out_num, out_den));
|
||||
|
||||
auto *db = new OakEngineTraverseDb;
|
||||
// A bare output table has no input id; represented as a single entry
|
||||
// with an empty id (see traverse.h).
|
||||
db->inputs.append(convert_input(QString(), table));
|
||||
return db;
|
||||
}
|
||||
|
||||
void oakengine_traverse_db_free(OakEngineTraverseDb *db)
|
||||
{
|
||||
delete db;
|
||||
}
|
||||
|
||||
int oakengine_traverse_db_input_count(const OakEngineTraverseDb *db)
|
||||
{
|
||||
return db ? int(db->inputs.size()) : 0;
|
||||
}
|
||||
|
||||
const char *oakengine_traverse_db_input_id(const OakEngineTraverseDb *db,
|
||||
int input_index)
|
||||
{
|
||||
if (!db || input_index < 0 || input_index >= db->inputs.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return db->inputs.at(input_index).id.constData();
|
||||
}
|
||||
|
||||
int oakengine_traverse_db_row_count(const OakEngineTraverseDb *db,
|
||||
int input_index)
|
||||
{
|
||||
if (!db || input_index < 0 || input_index >= db->inputs.size()) {
|
||||
return 0;
|
||||
}
|
||||
return int(db->inputs.at(input_index).rows.size());
|
||||
}
|
||||
|
||||
int oakengine_traverse_row_type(const OakEngineTraverseDb *db, int input_index,
|
||||
int row)
|
||||
{
|
||||
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
|
||||
return r ? r->type : OAK_NODE_VALUE_NONE;
|
||||
}
|
||||
|
||||
OakEngineNode *oakengine_traverse_row_source(const OakEngineTraverseDb *db,
|
||||
int input_index, int row)
|
||||
{
|
||||
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
|
||||
return (r && r->source) ?
|
||||
reinterpret_cast<OakEngineNode *>(
|
||||
const_cast<olive::Node *>(r->source)) :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
const char *oakengine_traverse_row_tag(const OakEngineTraverseDb *db,
|
||||
int input_index, int row)
|
||||
{
|
||||
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
|
||||
static const char empty[] = "";
|
||||
return r ? r->tag.constData() : empty;
|
||||
}
|
||||
|
||||
const char *oakengine_traverse_row_value_string(const OakEngineTraverseDb *db,
|
||||
int input_index, int row)
|
||||
{
|
||||
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
|
||||
return r ? r->value_string.constData() : nullptr;
|
||||
}
|
||||
|
||||
int oakengine_traverse_row_split_count(const OakEngineTraverseDb *db,
|
||||
int input_index, int row)
|
||||
{
|
||||
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
|
||||
return r ? int(r->splits.size()) : 0;
|
||||
}
|
||||
|
||||
const char *oakengine_traverse_row_split_string(const OakEngineTraverseDb *db,
|
||||
int input_index, int row,
|
||||
int split)
|
||||
{
|
||||
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
|
||||
if (!r || split < 0 || split >= int(r->splits.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
return r->splits[size_t(split)].constData();
|
||||
}
|
||||
|
||||
int oakengine_traverse_table_element_index_for_hint(
|
||||
OakEngineNode *hint_node, const char *input_id, int element,
|
||||
const OakEngineTraverseDb *table_db)
|
||||
{
|
||||
if (!hint_node || !input_id || !table_db || table_db->inputs.size() != 1) {
|
||||
return -1;
|
||||
}
|
||||
olive::NodeTraverser traverser;
|
||||
return traverser.generate_row_value_element_index(
|
||||
impl(hint_node), QString::fromUtf8(input_id), element,
|
||||
&table_db->inputs.first().table);
|
||||
}
|
||||
|
||||
int oakengine_traverse_generate_row(OakEngineNode *node, int64_t in_num,
|
||||
int64_t in_den, int64_t out_num,
|
||||
int64_t out_den,
|
||||
const oak_video_params *cache_video_params,
|
||||
int sample_rate, uint64_t channel_layout,
|
||||
void *row_out)
|
||||
{
|
||||
if (!node || !row_out || in_den == 0 || out_den == 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::NodeTraverser traverser;
|
||||
if (cache_video_params) {
|
||||
traverser.set_cache_video_params(to_cpp(*cache_video_params));
|
||||
}
|
||||
if (sample_rate > 0) {
|
||||
traverser.set_cache_audio_params(
|
||||
olive::AudioParams(sample_rate, channel_layout,
|
||||
olive::core::SampleFormat::f32_p));
|
||||
}
|
||||
// Transition bridge: row_out is the application's own olive::NodeValueRow
|
||||
// (a QHash typedef), filled in place.
|
||||
auto *row = static_cast<olive::NodeValueRow *>(row_out);
|
||||
*row = traverser.generate_row(impl(node),
|
||||
to_range(in_num, in_den, out_num, out_den));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_traverse_transform(OakEngineNode *start, OakEngineNode *end,
|
||||
int64_t in_num, int64_t in_den,
|
||||
int64_t out_num, int64_t out_den,
|
||||
const oak_video_params *cache_video_params,
|
||||
double out_m[6])
|
||||
{
|
||||
if (!start || !end || !out_m || in_den == 0 || out_den == 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::NodeTraverser traverser;
|
||||
if (cache_video_params) {
|
||||
traverser.set_cache_video_params(to_cpp(*cache_video_params));
|
||||
}
|
||||
QTransform t;
|
||||
traverser.transform(&t, impl(start), impl(end),
|
||||
to_range(in_num, in_den, out_num, out_den));
|
||||
out_m[0] = t.m11();
|
||||
out_m[1] = t.m12();
|
||||
out_m[2] = t.m21();
|
||||
out_m[3] = t.m22();
|
||||
out_m[4] = t.dx();
|
||||
out_m[5] = t.dy();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,711 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/undo.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QAction>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "olive/core/util/timecodefunctions.h"
|
||||
#include "coreengine.h"
|
||||
#include "node/block/block.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
#include "node/block/transition/transition.h"
|
||||
#include "node/nodeundo.h"
|
||||
#include "node/output/track/track.h"
|
||||
#include "node/output/track/tracklist.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "node/value.h"
|
||||
#include "timeline/timelinecommon.h"
|
||||
#include "timeline/timelineundogeneral.h"
|
||||
#include "timeline/timelineundopointer.h"
|
||||
#include "timeline/timelineundoripple.h"
|
||||
#include "timeline/timelineundosplit.h"
|
||||
#include "undo/undocommand.h"
|
||||
#include "undo/undostack.h"
|
||||
#include "undointernal.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::UndoStack *stack()
|
||||
{
|
||||
if (olive::EngineCore *core = olive::EngineCore::instance()) {
|
||||
return core->undo_stack();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// buf/size string writer (same convention as capi/project.cpp).
|
||||
int write_string(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf8 = s.toUtf8();
|
||||
const int len = int(utf8.size());
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = qMin(len, buf_size - 1);
|
||||
std::memcpy(buf, utf8.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Current open undo group. Owned by this TU; the facade owns it between
|
||||
// group_begin and group_end/group_abort.
|
||||
olive::MultiUndoCommand *g_undo_group = nullptr;
|
||||
QString g_undo_group_name;
|
||||
|
||||
} // namespace
|
||||
|
||||
olive::MultiUndoCommand *oakengine_undo_group_current(void)
|
||||
{
|
||||
return g_undo_group;
|
||||
}
|
||||
|
||||
// Shared helper used by all capi TU push_or_run() locals.
|
||||
void oakengine_undo_push_or_run(olive::UndoCommand *command, const QString &name)
|
||||
{
|
||||
if (olive::MultiUndoCommand *group = g_undo_group) {
|
||||
group->add_child(command);
|
||||
command->redo_now();
|
||||
} else if (olive::UndoStack *s = stack()) {
|
||||
s->push(command, name);
|
||||
} else {
|
||||
command->redo_now();
|
||||
delete command;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_undo_handle(void)
|
||||
{
|
||||
return stack();
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_push(void *command, const char *name)
|
||||
{
|
||||
if (!command) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
auto *cmd = static_cast<olive::UndoCommand *>(command);
|
||||
const QString label = name ? QString::fromUtf8(name) : QString();
|
||||
if (olive::MultiUndoCommand *group = g_undo_group) {
|
||||
group->add_child(cmd);
|
||||
cmd->redo_now();
|
||||
} else if (olive::UndoStack *s = stack()) {
|
||||
s->push(cmd, label);
|
||||
} else {
|
||||
cmd->redo_now();
|
||||
delete cmd;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_group_begin(const char *name)
|
||||
{
|
||||
if (g_undo_group) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
g_undo_group = new olive::MultiUndoCommand();
|
||||
g_undo_group_name = name ? QString::fromUtf8(name) : QString();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_group_end(void)
|
||||
{
|
||||
if (!g_undo_group) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
olive::MultiUndoCommand *group = g_undo_group;
|
||||
g_undo_group = nullptr;
|
||||
|
||||
QString name = g_undo_group_name;
|
||||
g_undo_group_name.clear();
|
||||
|
||||
olive::UndoStack *s = stack();
|
||||
if (!s) {
|
||||
// No stack: just redo/undo nothing and delete.
|
||||
delete group;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
// Empty group is discarded by push_pre_executed (mirrors push).
|
||||
s->push_pre_executed(group, name);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_group_abort(void)
|
||||
{
|
||||
if (!g_undo_group) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
olive::MultiUndoCommand *group = g_undo_group;
|
||||
g_undo_group = nullptr;
|
||||
g_undo_group_name.clear();
|
||||
|
||||
group->undo_now();
|
||||
delete group;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_command_redo_now(void *command)
|
||||
{
|
||||
if (!command) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
static_cast<olive::UndoCommand *>(command)->redo_now();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_command_undo_now(void *command)
|
||||
{
|
||||
if (!command) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
static_cast<olive::UndoCommand *>(command)->undo_now();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class CustomUndoCommand : public olive::UndoCommand {
|
||||
public:
|
||||
CustomUndoCommand(const QString &name,
|
||||
oakengine_undo_command_redo_fn redo_cb,
|
||||
oakengine_undo_command_undo_fn undo_cb,
|
||||
oakengine_undo_command_free_fn free_cb,
|
||||
void *userdata)
|
||||
: name_(name)
|
||||
, redo_fn_(redo_cb)
|
||||
, undo_fn_(undo_cb)
|
||||
, free_fn_(free_cb)
|
||||
, userdata_(userdata)
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~CustomUndoCommand() override
|
||||
{
|
||||
if (free_fn_) {
|
||||
free_fn_(userdata_);
|
||||
}
|
||||
}
|
||||
|
||||
virtual olive::Project *get_relevant_project() const override
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
if (redo_fn_) {
|
||||
redo_fn_(userdata_);
|
||||
}
|
||||
}
|
||||
|
||||
virtual void undo() override
|
||||
{
|
||||
if (undo_fn_) {
|
||||
undo_fn_(userdata_);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QString name_;
|
||||
oakengine_undo_command_redo_fn redo_fn_;
|
||||
oakengine_undo_command_undo_fn undo_fn_;
|
||||
oakengine_undo_command_free_fn free_fn_;
|
||||
void *userdata_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Map oak_node_value_type -> olive::NodeValue::Type (mirrors node.cpp).
|
||||
olive::NodeValue::Type from_c_type(int t)
|
||||
{
|
||||
switch (t) {
|
||||
case 0: return olive::NodeValue::k_none;
|
||||
case 1: return olive::NodeValue::k_int;
|
||||
case 2: return olive::NodeValue::k_float;
|
||||
case 3: return olive::NodeValue::k_boolean;
|
||||
case 4: return olive::NodeValue::k_rational;
|
||||
case 5: return olive::NodeValue::k_color;
|
||||
case 6: return olive::NodeValue::k_vec2;
|
||||
case 7: return olive::NodeValue::k_vec3;
|
||||
case 8: return olive::NodeValue::k_vec4;
|
||||
case 9: return olive::NodeValue::k_combo;
|
||||
case 10: return olive::NodeValue::k_file;
|
||||
case 11: return olive::NodeValue::k_text;
|
||||
case 12: return olive::NodeValue::k_font;
|
||||
case 13: return olive::NodeValue::k_str_combo;
|
||||
case 14: return olive::NodeValue::k_binary;
|
||||
case 15: return olive::NodeValue::k_bezier;
|
||||
case 16: return olive::NodeValue::k_texture;
|
||||
case 17: return olive::NodeValue::k_samples;
|
||||
case 18: return olive::NodeValue::k_video_params;
|
||||
case 19: return olive::NodeValue::k_audio_params;
|
||||
default: return olive::NodeValue::k_none;
|
||||
}
|
||||
}
|
||||
|
||||
const olive::Sequence *sequence_from_block(const olive::Block *block)
|
||||
{
|
||||
if (!block || !block->track()) {
|
||||
return nullptr;
|
||||
}
|
||||
return block->track()->sequence();
|
||||
}
|
||||
|
||||
olive::Rational sequence_time_base(const olive::Sequence *seq)
|
||||
{
|
||||
if (seq) {
|
||||
const olive::Rational fr = seq->get_video_params().frame_rate();
|
||||
if (!fr.isNull() && !fr.isNaN()) {
|
||||
return fr.flipped();
|
||||
}
|
||||
}
|
||||
return olive::Rational(1001, 30000);
|
||||
}
|
||||
|
||||
olive::Rational ts_to_time(int64_t ts, const olive::Rational &tb)
|
||||
{
|
||||
return olive::core::Timecode::timestamp_to_time(ts, tb);
|
||||
}
|
||||
|
||||
olive::Timeline::MovementMode to_movement_mode(int mode)
|
||||
{
|
||||
switch (mode) {
|
||||
case 1: return olive::Timeline::k_move;
|
||||
case 2: return olive::Timeline::k_trim_in;
|
||||
case 3: return olive::Timeline::k_trim_out;
|
||||
default: return olive::Timeline::k_none;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void *oakengine_undo_command_create(
|
||||
const char *name,
|
||||
oakengine_undo_command_redo_fn redo,
|
||||
oakengine_undo_command_undo_fn undo,
|
||||
oakengine_undo_command_free_fn free_fn,
|
||||
void *userdata)
|
||||
{
|
||||
return new CustomUndoCommand(
|
||||
name ? QString::fromUtf8(name) : QString(),
|
||||
redo, undo, free_fn, userdata);
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_undo_command_create_multi(void)
|
||||
{
|
||||
return new olive::MultiUndoCommand();
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_node_add_command(void *project, void *node)
|
||||
{
|
||||
if (!project || !node) {
|
||||
return nullptr;
|
||||
}
|
||||
return new olive::NodeAddCommand(
|
||||
reinterpret_cast<olive::Project *>(project),
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_node_set_position_command(
|
||||
void *node, void *context, double x, double y, int expanded)
|
||||
{
|
||||
if (!node || !context) {
|
||||
return nullptr;
|
||||
}
|
||||
return new olive::NodeSetPositionCommand(
|
||||
reinterpret_cast<olive::Node *>(node),
|
||||
reinterpret_cast<olive::Node *>(context),
|
||||
olive::Node::Position(QPointF(x, y), expanded != 0));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_node_remove_position_command(
|
||||
void *node, void *context)
|
||||
{
|
||||
if (!node || !context) {
|
||||
return nullptr;
|
||||
}
|
||||
return new olive::NodeRemovePositionFromContextCommand(
|
||||
reinterpret_cast<olive::Node *>(node),
|
||||
reinterpret_cast<olive::Node *>(context));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_node_set_value_hint_command(
|
||||
void *node, const char *input, int element, int type, int index,
|
||||
const char *tag)
|
||||
{
|
||||
if (!node || !input) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Node *n = reinterpret_cast<olive::Node *>(node);
|
||||
const QString id = QString::fromUtf8(input);
|
||||
if (!n->inputs().contains(id)) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::NodeValue::Type nv_type = olive::NodeValue::k_none;
|
||||
if (type >= 0) {
|
||||
nv_type = from_c_type(type);
|
||||
if (nv_type == olive::NodeValue::k_none && type != 0) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
QVector<olive::NodeValue::Type> types;
|
||||
if (nv_type != olive::NodeValue::k_none) {
|
||||
types.append(nv_type);
|
||||
}
|
||||
return new olive::NodeSetValueHintCommand(
|
||||
n, id, element,
|
||||
olive::Node::ValueHint(types, index, QString::fromUtf8(tag ? tag : "")));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_node_remove_and_disconnect_command(void *node)
|
||||
{
|
||||
if (!node) {
|
||||
return nullptr;
|
||||
}
|
||||
return new olive::NodeRemoveAndDisconnectCommand(
|
||||
reinterpret_cast<olive::Node *>(node));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_track_place_block_command(
|
||||
void *track_list, int track_index, void *block, int64_t in_ts)
|
||||
{
|
||||
if (!track_list || !block || in_ts < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::TrackList *list = reinterpret_cast<olive::TrackList *>(track_list);
|
||||
olive::Block *b = reinterpret_cast<olive::Block *>(block);
|
||||
const olive::Rational tb = sequence_time_base(list->parent());
|
||||
return new olive::TrackPlaceBlockCommand(
|
||||
list, track_index, b, ts_to_time(in_ts, tb));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_track_replace_block_with_gap_command(
|
||||
void *track, void *block, int handle_transitions)
|
||||
{
|
||||
if (!track || !block) {
|
||||
return nullptr;
|
||||
}
|
||||
return new olive::TrackReplaceBlockWithGapCommand(
|
||||
reinterpret_cast<olive::Track *>(track),
|
||||
reinterpret_cast<olive::Block *>(block),
|
||||
handle_transitions != 0);
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_block_trim_command(
|
||||
void *track, void *block, int64_t new_length_num, int64_t new_length_den,
|
||||
int movement_mode, int roll_edit)
|
||||
{
|
||||
if (!track || !block || new_length_den == 0 ||
|
||||
movement_mode < 0 || movement_mode > 3) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Track *t = reinterpret_cast<olive::Track *>(track);
|
||||
olive::Block *b = reinterpret_cast<olive::Block *>(block);
|
||||
auto *cmd = new olive::BlockTrimCommand(
|
||||
t, b,
|
||||
olive::Rational(static_cast<int>(new_length_num),
|
||||
static_cast<int>(new_length_den)),
|
||||
to_movement_mode(movement_mode));
|
||||
cmd->set_trim_is_a_roll_edit(roll_edit != 0);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_transition_remove_command(
|
||||
void *transition, int remove_from_graph)
|
||||
{
|
||||
if (!transition) {
|
||||
return nullptr;
|
||||
}
|
||||
return new olive::TransitionRemoveCommand(
|
||||
reinterpret_cast<olive::TransitionBlock *>(transition),
|
||||
remove_from_graph != 0);
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_track_slide_command(
|
||||
void *track, void *const *blocks, int block_count,
|
||||
void *in_adjacent, void *out_adjacent,
|
||||
int64_t movement_num, int64_t movement_den)
|
||||
{
|
||||
if (!track || !blocks || block_count <= 0 || movement_den == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Track *t = reinterpret_cast<olive::Track *>(track);
|
||||
QList<olive::Block *> block_list;
|
||||
block_list.reserve(block_count);
|
||||
for (int i = 0; i < block_count; i++) {
|
||||
if (!blocks[i]) {
|
||||
return nullptr;
|
||||
}
|
||||
block_list.append(reinterpret_cast<olive::Block *>(blocks[i]));
|
||||
}
|
||||
return new olive::TrackSlideCommand(
|
||||
t, block_list,
|
||||
reinterpret_cast<olive::Block *>(in_adjacent),
|
||||
reinterpret_cast<olive::Block *>(out_adjacent),
|
||||
olive::Rational(static_cast<int>(movement_num),
|
||||
static_cast<int>(movement_den)));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_block_split_preserving_links_command(
|
||||
void *const *blocks, int count, int64_t point_ts)
|
||||
{
|
||||
if (!blocks || count <= 0 || point_ts < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
QVector<olive::Block *> block_vec;
|
||||
block_vec.reserve(count);
|
||||
const olive::Sequence *seq = nullptr;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (!blocks[i]) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Block *b = reinterpret_cast<olive::Block *>(blocks[i]);
|
||||
if (!seq) {
|
||||
seq = sequence_from_block(b);
|
||||
}
|
||||
block_vec.append(b);
|
||||
}
|
||||
const olive::Rational tb = sequence_time_base(seq);
|
||||
const olive::Rational point = ts_to_time(point_ts, tb);
|
||||
// BlockSplitPreservingLinksCommand takes a list of times, one per block.
|
||||
QList<olive::Rational> times;
|
||||
times.reserve(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.append(point);
|
||||
}
|
||||
return new olive::BlockSplitPreservingLinksCommand(block_vec, times);
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_block_split_get_split(
|
||||
void *command, void *block, int time_index)
|
||||
{
|
||||
if (!command || !block) {
|
||||
return nullptr;
|
||||
}
|
||||
auto *cmd = reinterpret_cast<olive::BlockSplitPreservingLinksCommand *>(
|
||||
command);
|
||||
return cmd->get_split(reinterpret_cast<olive::Block *>(block), time_index);
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_block_resize_with_media_in_command(
|
||||
void *block, int64_t length_num, int64_t length_den)
|
||||
{
|
||||
if (!block || length_den == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Block *b = reinterpret_cast<olive::Block *>(block);
|
||||
return new olive::BlockResizeWithMediaInCommand(
|
||||
b, olive::Rational(static_cast<int>(length_num),
|
||||
static_cast<int>(length_den)));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_block_set_media_in_command(
|
||||
void *block, int64_t media_in_num, int64_t media_in_den)
|
||||
{
|
||||
if (!block || media_in_den == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::ClipBlock *clip = reinterpret_cast<olive::ClipBlock *>(block);
|
||||
return new olive::BlockSetMediaInCommand(
|
||||
clip, olive::Rational(static_cast<int>(media_in_num),
|
||||
static_cast<int>(media_in_den)));
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_timeline_ripple_delete_gaps_command(
|
||||
void *sequence, const int64_t *range_in_ts, const int64_t *range_out_ts,
|
||||
const int *track_types, const int *track_indexes, int range_count)
|
||||
{
|
||||
if (!sequence || !range_in_ts || !range_out_ts ||
|
||||
!track_types || !track_indexes || range_count <= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Sequence *seq = reinterpret_cast<olive::Sequence *>(sequence);
|
||||
const olive::Rational tb = sequence_time_base(seq);
|
||||
olive::TimelineRippleDeleteGapsAtRegionsCommand::RangeList ranges;
|
||||
ranges.reserve(range_count);
|
||||
for (int i = 0; i < range_count; i++) {
|
||||
if (range_in_ts[i] < 0 || range_out_ts[i] <= range_in_ts[i] ||
|
||||
track_types[i] < 0 || track_types[i] > 2 ||
|
||||
track_indexes[i] < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::TrackList *list = seq->track_list(
|
||||
static_cast<olive::Track::Type>(track_types[i]));
|
||||
if (!list || track_indexes[i] >= list->get_track_count()) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::Track *track = list->get_track_at(track_indexes[i]);
|
||||
ranges.append(qMakePair(
|
||||
track,
|
||||
olive::TimeRange(ts_to_time(range_in_ts[i], tb),
|
||||
ts_to_time(range_out_ts[i], tb))));
|
||||
}
|
||||
return new olive::TimelineRippleDeleteGapsAtRegionsCommand(seq, ranges);
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_track_list_insert_gaps_command(
|
||||
void *track_list, int64_t point_num, int64_t point_den,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
if (!track_list || point_den == 0 || length_den == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::TrackList *list = reinterpret_cast<olive::TrackList *>(track_list);
|
||||
return new olive::TrackListInsertGaps(
|
||||
list, olive::Rational(static_cast<int>(point_num),
|
||||
static_cast<int>(point_den)),
|
||||
olive::Rational(static_cast<int>(length_num),
|
||||
static_cast<int>(length_den)));
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_command_multi_add_child(void *multi,
|
||||
void *child)
|
||||
{
|
||||
if (!multi || !child) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
static_cast<olive::MultiUndoCommand *>(multi)->add_child(
|
||||
static_cast<olive::UndoCommand *>(child));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_command_multi_child_count(void *multi)
|
||||
{
|
||||
if (!multi) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return static_cast<olive::MultiUndoCommand *>(multi)->child_count();
|
||||
}
|
||||
|
||||
extern "C" void oakengine_undo_command_free(void *command)
|
||||
{
|
||||
delete static_cast<olive::UndoCommand *>(command);
|
||||
}
|
||||
|
||||
extern "C" int64_t oakengine_undo_count(void)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
return s ? s->command_count() : OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
extern "C" int64_t oakengine_undo_index(void)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
return s ? s->done_count() : OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_command_text(int64_t row, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
if (!s) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (row < 0 || row >= s->command_count()) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
return write_string(s->command_name(row), buf, buf_size);
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_command_is_done(int64_t row)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
if (!s) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (row < 0 || row >= s->command_count()) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
return s->command_is_done(row) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_jump(int64_t index)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
if (!s) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
if (index < 0 || index > s->command_count()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
s->jump(size_t(index));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_clear(void)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
if (!s) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
s->clear();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_update_actions(void)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
if (!s) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
s->update_actions();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_can_undo(void)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
return s ? (s->can_undo() ? 1 : 0) : OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
extern "C" int oakengine_undo_can_redo(void)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
return s ? (s->can_redo() ? 1 : 0) : OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_undo_undo_action(void)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
return s ? static_cast<void *>(s->GetUndoAction()) : nullptr;
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_undo_redo_action(void)
|
||||
{
|
||||
olive::UndoStack *s = stack();
|
||||
return s ? static_cast<void *>(s->GetRedoAction()) : nullptr;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAKENGINE_UNDOINTERNAL_H
|
||||
#define OAKENGINE_UNDOINTERNAL_H
|
||||
|
||||
// Internal (not installed) shared declarations between undo.cpp and the
|
||||
// other capi translation units. The undo-group state lives in undo.cpp;
|
||||
// these helpers let node/timeline/etc. push commands into an active group
|
||||
// instead of directly onto the global undo stack.
|
||||
|
||||
namespace olive
|
||||
{
|
||||
class MultiUndoCommand;
|
||||
class UndoCommand;
|
||||
}
|
||||
|
||||
class QString;
|
||||
|
||||
// Returns the currently active undo group, or nullptr if no group is open.
|
||||
olive::MultiUndoCommand *oakengine_undo_group_current(void);
|
||||
|
||||
// Push `command` into the active group (eager redo) or onto the global undo
|
||||
// stack. No-op if no engine core exists.
|
||||
void oakengine_undo_push_or_run(olive::UndoCommand *command,
|
||||
const QString &name);
|
||||
|
||||
#endif // OAKENGINE_UNDOINTERNAL_H
|
||||
@@ -0,0 +1,619 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/viewer.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QVector>
|
||||
|
||||
#include "oakengine/timeline.h"
|
||||
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/output/track/track.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
#include "node/nodeundo.h"
|
||||
#include "node/param.h"
|
||||
#include "render/playbackcache.h"
|
||||
#include "render/framehashcache.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "timeline/timelineworkarea.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::Node *impl(OakEngineNode *h)
|
||||
{
|
||||
return reinterpret_cast<olive::Node *>(h);
|
||||
}
|
||||
|
||||
const olive::Node *impl(const OakEngineNode *h)
|
||||
{
|
||||
return reinterpret_cast<const olive::Node *>(h);
|
||||
}
|
||||
|
||||
// Validated viewer accessor; nullptr when the handle is not a viewer.
|
||||
olive::ViewerOutput *viewer_of(OakEngineNode *h)
|
||||
{
|
||||
return h ? dynamic_cast<olive::ViewerOutput *>(impl(h)) : nullptr;
|
||||
}
|
||||
|
||||
const olive::ViewerOutput *viewer_of(const OakEngineNode *h)
|
||||
{
|
||||
return h ? dynamic_cast<const olive::ViewerOutput *>(impl(h)) : nullptr;
|
||||
}
|
||||
|
||||
// ViewerOutput::get_playhead()/get_connected_waveform() are not const in the
|
||||
// engine; the facade keeps const-correct handles and casts locally (same
|
||||
// pattern as timeline.cpp's mutable_impl()).
|
||||
olive::ViewerOutput *mutable_viewer(const OakEngineNode *h)
|
||||
{
|
||||
return const_cast<olive::ViewerOutput *>(viewer_of(h));
|
||||
}
|
||||
|
||||
// olive::VideoParams -> oak_video_params POD (same mapping as
|
||||
// encoding.cpp's from_cpp()).
|
||||
void from_cpp(const olive::VideoParams &vp, oak_video_params *out)
|
||||
{
|
||||
out->width = vp.width();
|
||||
out->height = vp.height();
|
||||
out->time_base_num = vp.time_base().numerator();
|
||||
out->time_base_den = vp.time_base().denominator();
|
||||
out->format = int(vp.format());
|
||||
out->pixel_aspect_num = vp.pixel_aspect_ratio().numerator();
|
||||
out->pixel_aspect_den = vp.pixel_aspect_ratio().denominator();
|
||||
out->interlacing = int(vp.interlacing());
|
||||
out->color_range = int(vp.color_range());
|
||||
out->divider = vp.divider();
|
||||
out->video_type = int(vp.video_type());
|
||||
out->premultiplied_alpha = vp.premultiplied_alpha() ? 1 : 0;
|
||||
}
|
||||
|
||||
void rational_out(const olive::Rational &r, int64_t *num, int64_t *den)
|
||||
{
|
||||
if (num) {
|
||||
*num = r.numerator();
|
||||
}
|
||||
if (den) {
|
||||
*den = r.denominator();
|
||||
}
|
||||
}
|
||||
|
||||
// Track::Type values match the facade's OAKENGINE_TRACK_TYPE_* constants;
|
||||
// assert it and convert explicitly anyway (k_none should never appear in an
|
||||
// enabled-stream list).
|
||||
static_assert(int(olive::Track::k_video) == OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
"track type mismatch");
|
||||
static_assert(int(olive::Track::k_audio) == OAKENGINE_TRACK_TYPE_AUDIO,
|
||||
"track type mismatch");
|
||||
static_assert(int(olive::Track::k_subtitle) == OAKENGINE_TRACK_TYPE_SUBTITLE,
|
||||
"track type mismatch");
|
||||
|
||||
int to_c_track_type(olive::Track::Type t)
|
||||
{
|
||||
switch (t) {
|
||||
case olive::Track::k_video:
|
||||
return OAKENGINE_TRACK_TYPE_VIDEO;
|
||||
case olive::Track::k_audio:
|
||||
return OAKENGINE_TRACK_TYPE_AUDIO;
|
||||
case olive::Track::k_subtitle:
|
||||
return OAKENGINE_TRACK_TYPE_SUBTITLE;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
OakEngineNode *oakengine_viewer_from_node(OakEngineNode *node)
|
||||
{
|
||||
return viewer_of(node) ? node : nullptr;
|
||||
}
|
||||
|
||||
const OakEngineNode *oakengine_viewer_from_const_node(const OakEngineNode *node)
|
||||
{
|
||||
return viewer_of(node) ? node : nullptr;
|
||||
}
|
||||
|
||||
const char *oakengine_viewer_video_params_input_id(void)
|
||||
{
|
||||
static const QByteArray s =
|
||||
olive::ViewerOutput::k_video_params_input.toUtf8();
|
||||
return s.constData();
|
||||
}
|
||||
|
||||
const char *oakengine_viewer_audio_params_input_id(void)
|
||||
{
|
||||
static const QByteArray s =
|
||||
olive::ViewerOutput::k_audio_params_input.toUtf8();
|
||||
return s.constData();
|
||||
}
|
||||
|
||||
const char *oakengine_viewer_subtitle_params_input_id(void)
|
||||
{
|
||||
static const QByteArray s =
|
||||
olive::ViewerOutput::k_subtitle_params_input.toUtf8();
|
||||
return s.constData();
|
||||
}
|
||||
|
||||
const char *oakengine_viewer_texture_input_id(void)
|
||||
{
|
||||
static const QByteArray s = olive::ViewerOutput::k_texture_input.toUtf8();
|
||||
return s.constData();
|
||||
}
|
||||
|
||||
const char *oakengine_viewer_samples_input_id(void)
|
||||
{
|
||||
static const QByteArray s = olive::ViewerOutput::k_samples_input.toUtf8();
|
||||
return s.constData();
|
||||
}
|
||||
|
||||
int oakengine_viewer_default_sample_format(void)
|
||||
{
|
||||
return int(olive::core::SampleFormat::Format(
|
||||
olive::ViewerOutput::k_default_sample_format));
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_playhead(const OakEngineNode *self, int64_t *num,
|
||||
int64_t *den)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
rational_out(mutable_viewer(self)->get_playhead(), num, den);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_set_playhead(OakEngineNode *self, int64_t num,
|
||||
int64_t den)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
v->set_playhead(olive::Rational(num, den));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_set_video_params(OakEngineNode *self,
|
||||
const oak_video_params *params,
|
||||
int index)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || !params) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::VideoParams vp(
|
||||
params->width, params->height,
|
||||
olive::Rational(params->time_base_num, params->time_base_den),
|
||||
static_cast<olive::core::PixelFormat::Format>(params->format),
|
||||
olive::VideoParams::k_internal_channel_count,
|
||||
olive::Rational(params->pixel_aspect_num, params->pixel_aspect_den),
|
||||
static_cast<olive::VideoParams::Interlacing>(params->interlacing),
|
||||
params->divider);
|
||||
v->set_video_params(vp, index);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_set_audio_params(OakEngineNode *self, int sample_rate,
|
||||
uint64_t channel_layout, int format,
|
||||
int index)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::AudioParams ap;
|
||||
ap.set_sample_rate(sample_rate);
|
||||
ap.set_channel_layout(channel_layout);
|
||||
ap.set_format(static_cast<olive::core::SampleFormat::Format>(format));
|
||||
v->set_audio_params(ap, index);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_length(const OakEngineNode *self, int64_t *num,
|
||||
int64_t *den)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
rational_out(v->get_length(), num, den);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_video_length(const OakEngineNode *self, int64_t *num,
|
||||
int64_t *den)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
rational_out(v->get_video_length(), num, den);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_audio_length(const OakEngineNode *self, int64_t *num,
|
||||
int64_t *den)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
rational_out(v->get_audio_length(), num, den);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_video_params(const OakEngineNode *self, int index,
|
||||
oak_video_params *out)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || !out) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
from_cpp(v->get_video_params(index), out);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_audio_params(const OakEngineNode *self, int index,
|
||||
int *sample_rate,
|
||||
uint64_t *channel_layout, int *format)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (sample_rate) {
|
||||
*sample_rate = 0;
|
||||
}
|
||||
if (channel_layout) {
|
||||
*channel_layout = 0;
|
||||
}
|
||||
if (format) {
|
||||
*format = 0;
|
||||
}
|
||||
if (index < 0 || index >= v->get_audio_stream_count()) {
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
const olive::AudioParams params = v->get_audio_params(index);
|
||||
if (sample_rate) {
|
||||
*sample_rate = params.sample_rate();
|
||||
}
|
||||
if (channel_layout) {
|
||||
*channel_layout = params.channel_layout();
|
||||
}
|
||||
if (format) {
|
||||
*format = int(olive::core::SampleFormat::Format(params.format()));
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_video_stream_count(const OakEngineNode *self)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
return v ? v->get_video_stream_count() : 0;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_audio_stream_count(const OakEngineNode *self)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
return v ? v->get_audio_stream_count() : 0;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_subtitle_stream_count(const OakEngineNode *self)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
return v ? v->get_subtitle_stream_count() : 0;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_stream_enabled(const OakEngineNode *self,
|
||||
int track_type, int index)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
switch (track_type) {
|
||||
case OAKENGINE_TRACK_TYPE_VIDEO:
|
||||
return index >= 0 && index < v->get_video_stream_count() &&
|
||||
v->get_video_params(index).enabled();
|
||||
case OAKENGINE_TRACK_TYPE_AUDIO:
|
||||
return index >= 0 && index < v->get_audio_stream_count() &&
|
||||
v->get_audio_params(index).enabled();
|
||||
case OAKENGINE_TRACK_TYPE_SUBTITLE:
|
||||
return index >= 0 && index < v->get_subtitle_stream_count() &&
|
||||
v->get_subtitle_params(index).enabled();
|
||||
default:
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_subtitle_count(const OakEngineNode *self, int index)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || index < 0 || index >= v->get_subtitle_stream_count()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
return int(v->get_subtitle_params(index).size());
|
||||
}
|
||||
|
||||
const void *oakengine_viewer_get_subtitle_at(const OakEngineNode *self,
|
||||
int index, int sub_index)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || index < 0 || index >= v->get_subtitle_stream_count()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const olive::SubtitleParams &sp = v->get_subtitle_params(index);
|
||||
if (sub_index < 0 || sub_index >= int(sp.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &sp[size_t(sub_index)];
|
||||
}
|
||||
|
||||
int oakengine_viewer_has_enabled_streams(const OakEngineNode *self,
|
||||
int track_type)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return 0;
|
||||
}
|
||||
switch (track_type) {
|
||||
case OAKENGINE_TRACK_TYPE_VIDEO:
|
||||
return v->has_enabled_video_streams() ? 1 : 0;
|
||||
case OAKENGINE_TRACK_TYPE_AUDIO:
|
||||
return v->has_enabled_audio_streams() ? 1 : 0;
|
||||
case OAKENGINE_TRACK_TYPE_SUBTITLE:
|
||||
return v->has_enabled_subtitle_streams() ? 1 : 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_first_enabled_video_stream(const OakEngineNode *self,
|
||||
oak_video_params *out)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || !out) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
from_cpp(v->get_first_enabled_video_stream(), out);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_enabled_stream_count(const OakEngineNode *self)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
return v ? int(v->get_enabled_streams_as_references().size()) : 0;
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_enabled_streams(const OakEngineNode *self, int *types,
|
||||
int *indices, int max)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || max < 0) {
|
||||
return 0;
|
||||
}
|
||||
const QVector<olive::Track::Reference> refs =
|
||||
v->get_enabled_streams_as_references();
|
||||
if (types && indices) {
|
||||
const int n = qMin(int(refs.size()), max);
|
||||
for (int i = 0; i < n; i++) {
|
||||
types[i] = to_c_track_type(refs.at(i).type());
|
||||
indices[i] = refs.at(i).index();
|
||||
}
|
||||
}
|
||||
return int(refs.size());
|
||||
}
|
||||
|
||||
int oakengine_viewer_get_workarea(const OakEngineNode *self,
|
||||
oakengine_viewer_workarea *out)
|
||||
{
|
||||
const olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || !out) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::TimelineWorkArea *workarea = v->get_work_area();
|
||||
out->in_num = workarea->in().numerator();
|
||||
out->in_den = workarea->in().denominator();
|
||||
out->out_num = workarea->out().numerator();
|
||||
out->out_den = workarea->out().denominator();
|
||||
out->enabled = workarea->enabled() ? 1 : 0;
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_set_workarea_range(OakEngineNode *self, int64_t in_num,
|
||||
int64_t in_den, int64_t out_num,
|
||||
int64_t out_den)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
v->get_work_area()->set_range(
|
||||
olive::TimeRange(olive::Rational(in_num, in_den),
|
||||
olive::Rational(out_num, out_den)));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_set_workarea_enabled(OakEngineNode *self, int enabled)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
v->get_work_area()->set_enabled(enabled != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_set_default_parameters(OakEngineNode *self)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
v->set_default_parameters();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
extern "C" void *oakengine_viewer_set_preview_divider_command(
|
||||
OakEngineNode *self, int divider)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || divider < 1) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::VideoParams current = v->get_video_params();
|
||||
if (current.divider() == divider) {
|
||||
return nullptr;
|
||||
}
|
||||
const olive::VideoParams updated(
|
||||
current.width(), current.height(), current.time_base(),
|
||||
current.format(), current.channel_count(),
|
||||
current.pixel_aspect_ratio(), current.interlacing(), divider);
|
||||
return new olive::NodeParamSetStandardValueCommand(
|
||||
olive::NodeKeyframeTrackReference(
|
||||
olive::NodeInput(v, olive::ViewerOutput::k_video_params_input), 0),
|
||||
QVariant::fromValue(updated));
|
||||
}
|
||||
|
||||
int oakengine_viewer_set_parameters_from_footage(
|
||||
OakEngineNode *self, OakEngineNode *const *footage, int count)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v || count < 0 || (count > 0 && !footage)) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
QVector<olive::ViewerOutput *> viewers;
|
||||
viewers.reserve(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
olive::ViewerOutput *f = viewer_of(footage[i]);
|
||||
if (!f) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
viewers.append(f);
|
||||
}
|
||||
v->set_parameters_from_footage(viewers);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_viewer_set_waveform_enabled(OakEngineNode *self, int enabled)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
v->set_waveform_enabled(enabled != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
const void *oakengine_viewer_get_connected_waveform(const OakEngineNode *self)
|
||||
{
|
||||
olive::ViewerOutput *v = mutable_viewer(self);
|
||||
if (!v) {
|
||||
return nullptr;
|
||||
}
|
||||
return static_cast<const void *>(v->get_connected_waveform());
|
||||
}
|
||||
|
||||
OakEngineMarkerList *oakengine_viewer_get_marker_list(OakEngineNode *self)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<OakEngineMarkerList *>(v->get_markers());
|
||||
}
|
||||
|
||||
OakEngineWorkarea *oakengine_viewer_get_workarea_handle(OakEngineNode *self)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<OakEngineWorkarea *>(v->get_work_area());
|
||||
}
|
||||
|
||||
/* ---- Playback cache / frame cache ------------------------------------------ */
|
||||
|
||||
OakEnginePlaybackCache *
|
||||
oakengine_viewer_get_playback_cache(OakEngineNode *self)
|
||||
{
|
||||
if (!self) {
|
||||
return nullptr;
|
||||
}
|
||||
olive::ClipBlock *clip = dynamic_cast<olive::ClipBlock *>(
|
||||
reinterpret_cast<olive::Node *>(self));
|
||||
if (!clip) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<OakEnginePlaybackCache *>(
|
||||
clip->connected_video_cache());
|
||||
}
|
||||
|
||||
int oakengine_playback_cache_indicator_height(void)
|
||||
{
|
||||
return olive::PlaybackCache::get_cache_indicator_height();
|
||||
}
|
||||
|
||||
int oakengine_playback_cache_valid_ranges(OakEnginePlaybackCache *cache,
|
||||
int64_t *ranges, int max)
|
||||
{
|
||||
if (!cache) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::PlaybackCache *pc = reinterpret_cast<olive::PlaybackCache *>(cache);
|
||||
const olive::TimeRangeList &valid = pc->get_validated_ranges();
|
||||
const int count = qMin(max, int(valid.size()));
|
||||
for (int i = 0; i < count; i++) {
|
||||
ranges[i * 4 + 0] = valid.at(i).in().numerator();
|
||||
ranges[i * 4 + 1] = valid.at(i).in().denominator();
|
||||
ranges[i * 4 + 2] = valid.at(i).out().numerator();
|
||||
ranges[i * 4 + 3] = valid.at(i).out().denominator();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
OakEngineFrameCache *oakengine_viewer_get_frame_cache(OakEngineNode *self)
|
||||
{
|
||||
olive::ViewerOutput *v = viewer_of(self);
|
||||
if (!v) {
|
||||
return nullptr;
|
||||
}
|
||||
// For a clip, get its connected video cache as a FrameHashCache.
|
||||
if (olive::ClipBlock *clip = dynamic_cast<olive::ClipBlock *>(v)) {
|
||||
return reinterpret_cast<OakEngineFrameCache *>(
|
||||
clip->connected_video_cache());
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,945 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/worker.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <csignal>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMatrix4x4>
|
||||
#include <QOpenGLContext>
|
||||
#include <QSurfaceFormat>
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <execinfo.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "common/qtutils.h"
|
||||
#include "config/config.h"
|
||||
#include "coreengine.h"
|
||||
#include "node/factory.h"
|
||||
#include "node/input/multicam/multicamnode.h"
|
||||
#include "node/project/serializer/serializer.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/framemanager.h"
|
||||
#include "render/ipc/frameslotpool.h"
|
||||
#include "render/ipc/ipcmessage.h"
|
||||
#include "render/ipc/sharedmemoryregion.h"
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
#include "render/backend/dynamicrenderer.h"
|
||||
#endif
|
||||
#include "render/opengl/openglrenderer.h"
|
||||
#include "render/rendermanager.h"
|
||||
#include "render/renderprocessor.h"
|
||||
#include "render/colorprocessor.h"
|
||||
#include "render/colortransform.h"
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
void HideWorkerDockIcon();
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
void print_backtrace(int sig)
|
||||
{
|
||||
void *array[50];
|
||||
size_t size = backtrace(array, 50);
|
||||
fprintf(stderr, "worker: caught signal %d, backtrace:\n", sig);
|
||||
backtrace_symbols_fd(array, size, STDERR_FILENO);
|
||||
fflush(stderr);
|
||||
_exit(128 + sig);
|
||||
}
|
||||
#endif
|
||||
|
||||
constexpr int k_protocol_version = 1;
|
||||
constexpr int k_default_width = 1920;
|
||||
constexpr int k_default_height = 1080;
|
||||
constexpr int k_default_frame_rate = 24;
|
||||
|
||||
void install_surface_format()
|
||||
{
|
||||
QSurfaceFormat format;
|
||||
format.setVersion(3, 2);
|
||||
format.setProfile(QSurfaceFormat::CoreProfile);
|
||||
format.setDepthBufferSize(24);
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
}
|
||||
|
||||
void log_error(const QString &message)
|
||||
{
|
||||
const QByteArray line = QByteArray("worker: ") + message.toUtf8() + '\n';
|
||||
fwrite(line.constData(), 1, size_t(line.size()), stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
QJsonObject error_message(const QString &message, qint64 ticket_id = 0)
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = olive::ipc::msgtype::k_error;
|
||||
o["message"] = message;
|
||||
if (ticket_id) {
|
||||
o["ticket"] = double(ticket_id);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief Engine-internal render worker session.
|
||||
*
|
||||
* Holds the whole worker-side state machine (renderer, loaded project,
|
||||
* shared-memory frame pools, shader/color caches) and answers one NDJSON
|
||||
* control message at a time. Responses that the process main loop would
|
||||
* write to stdout are produced into `response` instead, so the session is
|
||||
* transport-agnostic and unit-testable. Owned via the OakWorkerSession C
|
||||
* handle.
|
||||
*/
|
||||
struct __attribute__((visibility("hidden"))) OakWorkerSession {
|
||||
olive::Renderer *renderer = nullptr;
|
||||
bool shutdown_requested = false;
|
||||
bool runtime_initialized = false;
|
||||
std::unique_ptr<olive::Project> project;
|
||||
QHash<QString, olive::Node *> node_by_token;
|
||||
olive::ipc::SharedMemoryRegion output_region;
|
||||
std::optional<olive::ipc::FrameSlotPool> output_pool;
|
||||
olive::ipc::SharedMemoryRegion input_region;
|
||||
std::optional<olive::ipc::FrameSlotPool> input_pool;
|
||||
olive::ShaderCache shader_cache;
|
||||
QHash<QString, olive::ColorProcessorPtr> color_processor_cache;
|
||||
|
||||
~OakWorkerSession()
|
||||
{
|
||||
project.reset();
|
||||
if (runtime_initialized) {
|
||||
olive::ProjectSerializer::destroy();
|
||||
olive::DiskManager::destroy_instance();
|
||||
olive::FrameManager::destroy_instance();
|
||||
olive::NodeFactory::destroy();
|
||||
}
|
||||
if (renderer) {
|
||||
renderer->destroy();
|
||||
renderer->post_destroy();
|
||||
delete renderer;
|
||||
}
|
||||
}
|
||||
|
||||
bool initialize_runtime()
|
||||
{
|
||||
if (runtime_initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The session API is also used without oakengine_worker_main() (unit
|
||||
// tests, embedded harnesses), where no QApplication exists yet.
|
||||
// Config/managers below require one (QSettings, QStandardPaths), so
|
||||
// create a minimal offscreen instance, mirroring oakengine_init().
|
||||
if (!QCoreApplication::instance()) {
|
||||
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
|
||||
qputenv("QT_QPA_PLATFORM", "offscreen");
|
||||
}
|
||||
static int argc = 1;
|
||||
static char app_name[] = "oak-worker";
|
||||
static char *argv[] = { app_name, nullptr };
|
||||
// Never deleted: QCoreApplication is a process-lifetime object.
|
||||
new QGuiApplication(argc, argv);
|
||||
QCoreApplication::setOrganizationName(
|
||||
QStringLiteral("oakvideoeditor.org"));
|
||||
QCoreApplication::setApplicationName(
|
||||
QStringLiteral("oak-render-worker"));
|
||||
}
|
||||
|
||||
// Create a minimal EngineCore instance so that code paths calling
|
||||
// EngineCore::instance() (e.g. ViewerOutput::data for timecode display)
|
||||
// do not dereference null. The worker has no UI, so the plain engine
|
||||
// core is sufficient. The worker is short-lived; leaking this on exit
|
||||
// is harmless.
|
||||
if (!olive::EngineCore::instance()) {
|
||||
new olive::EngineCore(olive::EngineCore::CoreParams());
|
||||
}
|
||||
|
||||
olive::Config::load();
|
||||
olive::NodeFactory::initialize();
|
||||
olive::ColorManager::set_up_default_config();
|
||||
olive::FrameManager::create_instance();
|
||||
olive::DiskManager::create_instance();
|
||||
olive::ProjectSerializer::initialize();
|
||||
runtime_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
QJsonObject startup_handshake() const
|
||||
{
|
||||
olive::ipc::HandshakeMsg hs;
|
||||
hs.protocol_version = k_protocol_version;
|
||||
hs.shm_key = QString();
|
||||
hs.input_shm_key = QString();
|
||||
hs.input_slots = 0;
|
||||
hs.output_slots = 0;
|
||||
hs.slot_data_bytes = 0;
|
||||
hs.input_slot_data_bytes = 0;
|
||||
|
||||
QJsonObject handshake = hs.to_json();
|
||||
if (QOpenGLContext *ctx = gl_context()) {
|
||||
const QSurfaceFormat fmt = ctx->format();
|
||||
handshake["gl_major"] = fmt.majorVersion();
|
||||
handshake["gl_minor"] = fmt.minorVersion();
|
||||
}
|
||||
return handshake;
|
||||
}
|
||||
|
||||
QOpenGLContext *gl_context() const
|
||||
{
|
||||
if (!renderer) {
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
if (auto *dynamic_renderer =
|
||||
dynamic_cast<olive::DynamicRenderer *>(renderer)) {
|
||||
return dynamic_renderer->open_gl_context();
|
||||
}
|
||||
#endif
|
||||
return static_cast<olive::OpenGLRenderer *>(renderer)->context();
|
||||
}
|
||||
|
||||
/// Handle one parsed control message. `response` is left untouched when
|
||||
/// the message has no reply (successful handshake, cancel, shutdown).
|
||||
/// Returns false only on an internal failure the main loop treats as
|
||||
/// fatal.
|
||||
bool handle(const QJsonObject &message, QJsonObject *response)
|
||||
{
|
||||
const QString type = message["type"].toString();
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::k_handshake)) {
|
||||
olive::ipc::HandshakeMsg hs;
|
||||
if (!olive::ipc::HandshakeMsg::from_json(message, &hs)) {
|
||||
*response =
|
||||
error_message(QStringLiteral("invalid handshake message"));
|
||||
return true;
|
||||
}
|
||||
return attach_output_pool(hs, response);
|
||||
}
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::k_load_graph)) {
|
||||
olive::ipc::LoadGraphMsg load;
|
||||
if (!olive::ipc::LoadGraphMsg::from_json(message, &load)) {
|
||||
*response =
|
||||
error_message(QStringLiteral("invalid load_graph message"));
|
||||
return true;
|
||||
}
|
||||
return load_graph(load.path, response);
|
||||
}
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::k_render_frame)) {
|
||||
olive::ipc::RenderFrameMsg render;
|
||||
if (!olive::ipc::RenderFrameMsg::from_json(message, &render)) {
|
||||
*response = error_message(
|
||||
QStringLiteral("invalid render_frame message"));
|
||||
return true;
|
||||
}
|
||||
return render_frame(render, response);
|
||||
}
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::k_cancel)) {
|
||||
// Stage 5 wires cancellation into in-flight jobs. Stage 2 has only synchronous single-frame work.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::k_shutdown)) {
|
||||
shutdown_requested = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
*response =
|
||||
error_message(QStringLiteral("unknown message type: %1").arg(type));
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool attach_output_pool(const olive::ipc::HandshakeMsg &hs,
|
||||
QJsonObject *response)
|
||||
{
|
||||
if (hs.protocol_version != k_protocol_version) {
|
||||
*response =
|
||||
error_message(QStringLiteral("unsupported protocol version %1")
|
||||
.arg(hs.protocol_version));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hs.shm_key.isEmpty() || hs.output_slots <= 0 ||
|
||||
hs.slot_data_bytes <= 0) {
|
||||
*response = error_message(QStringLiteral(
|
||||
"handshake missing output shared-memory geometry"));
|
||||
return true;
|
||||
}
|
||||
|
||||
const size_t bytes = olive::ipc::FrameSlotPool::bytes_needed(
|
||||
uint32_t(hs.output_slots), size_t(hs.slot_data_bytes));
|
||||
if (!output_region.open(hs.shm_key, bytes,
|
||||
olive::ipc::SharedMemoryRegion::k_attach)) {
|
||||
*response = error_message(
|
||||
QStringLiteral("failed to attach shared memory: %1")
|
||||
.arg(output_region.error()));
|
||||
return true;
|
||||
}
|
||||
|
||||
output_pool = olive::ipc::FrameSlotPool::attach(output_region.data());
|
||||
if (!output_pool->is_valid()) {
|
||||
output_region.close();
|
||||
output_pool.reset();
|
||||
*response = error_message(QStringLiteral(
|
||||
"shared memory does not contain a frame slot pool"));
|
||||
return true;
|
||||
}
|
||||
|
||||
input_pool.reset();
|
||||
input_region.close();
|
||||
if (hs.input_slots > 0) {
|
||||
if (hs.input_shm_key.isEmpty() || hs.input_slot_data_bytes <= 0) {
|
||||
*response = error_message(QStringLiteral(
|
||||
"handshake missing input shared-memory geometry"));
|
||||
return true;
|
||||
}
|
||||
|
||||
const size_t input_bytes = olive::ipc::FrameSlotPool::bytes_needed(
|
||||
uint32_t(hs.input_slots), size_t(hs.input_slot_data_bytes));
|
||||
if (!input_region.open(hs.input_shm_key, input_bytes,
|
||||
olive::ipc::SharedMemoryRegion::k_attach)) {
|
||||
*response = error_message(
|
||||
QStringLiteral("failed to attach input shared memory: %1")
|
||||
.arg(input_region.error()));
|
||||
return true;
|
||||
}
|
||||
|
||||
input_pool =
|
||||
olive::ipc::FrameSlotPool::attach(input_region.data());
|
||||
if (!input_pool->is_valid()) {
|
||||
input_region.close();
|
||||
input_pool.reset();
|
||||
*response = error_message(QStringLiteral(
|
||||
"input shared memory does not contain a frame slot pool"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load_graph(const QString &path, QJsonObject *response)
|
||||
{
|
||||
{
|
||||
QFileInfo fi(path);
|
||||
if (!fi.exists()) {
|
||||
log_error(
|
||||
QStringLiteral("LoadGraph: graph file does not exist: %1")
|
||||
.arg(path));
|
||||
*response = error_message(
|
||||
QStringLiteral("graph file does not exist: %1").arg(path));
|
||||
return true;
|
||||
}
|
||||
if (fi.size() == 0) {
|
||||
log_error(QStringLiteral("LoadGraph: graph file is empty: %1")
|
||||
.arg(path));
|
||||
*response = error_message(
|
||||
QStringLiteral("graph file is empty: %1").arg(path));
|
||||
return true;
|
||||
}
|
||||
log_error(
|
||||
QStringLiteral("LoadGraph: loading %1 (%2 bytes, readable=%3)")
|
||||
.arg(path)
|
||||
.arg(fi.size())
|
||||
.arg(fi.isReadable()));
|
||||
}
|
||||
|
||||
auto loaded = std::make_unique<olive::Project>();
|
||||
// Do not call Initialize() here: project serializers expect a blank
|
||||
// project (root_ == nullptr) and will set root themselves. Calling
|
||||
// Initialize() first triggers Q_ASSERT(!root_) in Project::Load.
|
||||
|
||||
olive::ProjectSerializer::Result result =
|
||||
olive::ProjectSerializer::load(loaded.get(), path,
|
||||
olive::ProjectSerializer::k_project);
|
||||
if (result != olive::ProjectSerializer::k_success) {
|
||||
*response =
|
||||
error_message(QStringLiteral("failed to load graph %1: %2")
|
||||
.arg(path, result.get_details()));
|
||||
return true;
|
||||
}
|
||||
|
||||
project = std::move(loaded);
|
||||
node_by_token.clear();
|
||||
color_processor_cache.clear();
|
||||
|
||||
const auto &data = result.get_load_data();
|
||||
for (auto it = data.node_ptrs.cbegin(); it != data.node_ptrs.cend();
|
||||
++it) {
|
||||
node_by_token.insert(QString::number(it.key()), it.value());
|
||||
}
|
||||
for (auto it = data.node_uuids.cbegin(); it != data.node_uuids.cend();
|
||||
++it) {
|
||||
node_by_token.insert(it.value().toString(), it.key());
|
||||
node_by_token.insert(it.value().toString(QUuid::WithoutBraces),
|
||||
it.key());
|
||||
}
|
||||
|
||||
QJsonObject ack;
|
||||
ack["type"] = QStringLiteral("graph_loaded");
|
||||
ack["nodes"] = node_by_token.size();
|
||||
*response = ack;
|
||||
return true;
|
||||
}
|
||||
|
||||
olive::Node *find_node(const QString &token) const
|
||||
{
|
||||
if (olive::Node *node = node_by_token.value(token, nullptr)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const quintptr ptr = token.toULongLong(&ok, 0);
|
||||
if (ok) {
|
||||
return node_by_token.value(QString::number(ptr), nullptr);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool render_frame(const olive::ipc::RenderFrameMsg &message,
|
||||
QJsonObject *response)
|
||||
{
|
||||
if (!project) {
|
||||
*response = error_message(
|
||||
QStringLiteral("render_frame received before load_graph"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
if (!output_pool || !output_pool->is_valid()) {
|
||||
*response = error_message(
|
||||
QStringLiteral(
|
||||
"render_frame received before output shm handshake"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
olive::Node *node = find_node(message.node_uuid);
|
||||
if (!node) {
|
||||
*response =
|
||||
error_message(QStringLiteral("render node not found: %1")
|
||||
.arg(message.node_uuid),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<int> input_slots;
|
||||
const QVector<int> requested_input_slots =
|
||||
message.input_slots.isEmpty() && message.input_slot >= 0 ?
|
||||
QVector<int>{ message.input_slot } :
|
||||
message.input_slots;
|
||||
if (!requested_input_slots.isEmpty()) {
|
||||
if (!input_pool || !input_pool->is_valid()) {
|
||||
*response = error_message(
|
||||
QStringLiteral(
|
||||
"render_frame referenced input slot without input pool"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int requested_slot : requested_input_slots) {
|
||||
if (requested_slot < 0 ||
|
||||
requested_slot >= int(input_pool->slot_count())) {
|
||||
for (int slot : input_slots) {
|
||||
input_pool->release(uint32_t(slot));
|
||||
}
|
||||
*response = error_message(
|
||||
QStringLiteral("input slot index out of range"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t consumed_slot = 0;
|
||||
if (!input_pool->consume(&consumed_slot)) {
|
||||
for (int slot : input_slots) {
|
||||
input_pool->release(uint32_t(slot));
|
||||
}
|
||||
*response =
|
||||
error_message(QStringLiteral("input slot was not ready"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
if (int(consumed_slot) != requested_slot) {
|
||||
input_pool->release(consumed_slot);
|
||||
for (int slot : input_slots) {
|
||||
input_pool->release(uint32_t(slot));
|
||||
}
|
||||
*response = error_message(
|
||||
QStringLiteral("input slot order mismatch"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
input_slots.append(int(consumed_slot));
|
||||
}
|
||||
}
|
||||
|
||||
olive::VideoParams vparams(
|
||||
message.width > 0 ? message.width : k_default_width,
|
||||
message.height > 0 ? message.height : k_default_height,
|
||||
olive::Rational(1, k_default_frame_rate),
|
||||
message.format >= 0 ? olive::PixelFormat::Format(message.format) :
|
||||
olive::PixelFormat::f32,
|
||||
message.channel_count > 0 ? message.channel_count :
|
||||
olive::VideoParams::k_rgba_channel_count);
|
||||
|
||||
olive::RenderTicketPtr ticket = std::make_shared<olive::RenderTicket>();
|
||||
ticket->setProperty("node", olive::QtUtils::ptr_to_value(node));
|
||||
ticket->setProperty("time",
|
||||
QVariant::fromValue(olive::Rational(
|
||||
int(message.time_num), int(message.time_den))));
|
||||
ticket->setProperty("size", QSize(message.width, message.height));
|
||||
ticket->setProperty("matrix", QMatrix4x4());
|
||||
ticket->setProperty("format",
|
||||
message.format >= 0 ?
|
||||
olive::PixelFormat::Format(message.format) :
|
||||
olive::PixelFormat::invalid);
|
||||
ticket->setProperty("usecache", false);
|
||||
ticket->setProperty("channelcount", message.channel_count);
|
||||
ticket->setProperty("mode", olive::RenderMode::Mode(message.mode));
|
||||
ticket->setProperty("type", olive::RenderManager::k_type_video);
|
||||
ticket->setProperty("colormanager", olive::QtUtils::ptr_to_value(
|
||||
project->color_manager()));
|
||||
|
||||
{
|
||||
olive::ColorProcessorPtr color_output;
|
||||
if (message.has_color_transform) {
|
||||
QString cache_key = QStringLiteral("%1|%2|%3|%4")
|
||||
.arg(message.color_is_display ? 1 : 0)
|
||||
.arg(message.color_output,
|
||||
message.color_view,
|
||||
message.color_look);
|
||||
auto it = color_processor_cache.find(cache_key);
|
||||
if (it != color_processor_cache.end()) {
|
||||
color_output = it.value();
|
||||
} else {
|
||||
olive::ColorTransform transform;
|
||||
if (message.color_is_display) {
|
||||
transform = olive::ColorTransform(message.color_output,
|
||||
message.color_view,
|
||||
message.color_look);
|
||||
} else {
|
||||
transform = olive::ColorTransform(message.color_output);
|
||||
}
|
||||
color_output = olive::ColorProcessor::create(
|
||||
project->color_manager(),
|
||||
project->color_manager()->get_reference_color_space(),
|
||||
transform);
|
||||
if (color_output) {
|
||||
color_processor_cache.insert(cache_key, color_output);
|
||||
}
|
||||
}
|
||||
}
|
||||
ticket->setProperty("coloroutput",
|
||||
QVariant::fromValue(color_output));
|
||||
}
|
||||
ticket->setProperty("vparam", QVariant::fromValue(vparams));
|
||||
// The IPC render_frame message carries no audio parameters, but
|
||||
// rendering a sequence that has audio content evaluates audio
|
||||
// tracks with globals.aparams -- an empty AudioParams aborts
|
||||
// (AudioParams::time_to_samples asserts is_valid). Use the render
|
||||
// node's own audio parameters, mirroring the in-process render
|
||||
// path (PreviewAutoCacher uses context->get_audio_params()).
|
||||
olive::AudioParams aparam;
|
||||
if (olive::ViewerOutput *viewer =
|
||||
dynamic_cast<olive::ViewerOutput *>(node)) {
|
||||
aparam = viewer->get_audio_params();
|
||||
}
|
||||
ticket->setProperty("aparam", QVariant::fromValue(aparam));
|
||||
ticket->setProperty("return", olive::RenderManager::k_frame);
|
||||
ticket->setProperty("cache", QString());
|
||||
ticket->setProperty("cachetimebase",
|
||||
QVariant::fromValue(olive::Rational(1)));
|
||||
ticket->setProperty("cacheid", QVariant::fromValue(QUuid()));
|
||||
ticket->setProperty("multicam", olive::QtUtils::ptr_to_value(
|
||||
static_cast<void *>(nullptr)));
|
||||
ticket->setProperty(
|
||||
"ipc_input_pool",
|
||||
// The engine reads this back as the internal implementation object
|
||||
// (olive::engine::internal::ipc::FrameSlotPool), which is exactly
|
||||
// what the C handle points at.
|
||||
olive::QtUtils::ptr_to_value(input_pool ?
|
||||
static_cast<void *>(
|
||||
input_pool->handle()) :
|
||||
static_cast<void *>(nullptr)));
|
||||
QVariantList input_slot_values;
|
||||
for (int slot : input_slots) {
|
||||
input_slot_values.append(slot);
|
||||
}
|
||||
ticket->setProperty("ipc_input_slots", input_slot_values);
|
||||
ticket->setProperty("ipc_input_slot_cursor", 0);
|
||||
ticket->setProperty("ipc_input_slot",
|
||||
input_slots.isEmpty() ? -1 : input_slots.front());
|
||||
|
||||
ticket->start();
|
||||
olive::RenderProcessor::process(ticket, renderer, nullptr,
|
||||
&shader_cache);
|
||||
for (int slot : input_slots) {
|
||||
input_pool->release(uint32_t(slot));
|
||||
}
|
||||
if (!ticket->has_result()) {
|
||||
*response = error_message(QStringLiteral("render produced no frame"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
olive::FramePtr frame = ticket->get().value<olive::FramePtr>();
|
||||
if (!frame || !frame->is_allocated()) {
|
||||
*response = error_message(QStringLiteral("render result was empty"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t slot = 0;
|
||||
if (!output_pool->acquire(&slot)) {
|
||||
*response =
|
||||
error_message(QStringLiteral("no free output frame slot"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
const int data_size = frame->linesize_bytes() * frame->height();
|
||||
if (data_size > int(output_pool->slot_data_bytes())) {
|
||||
output_pool->release(slot);
|
||||
log_error(QString("Output frame size") + QString::number(data_size));
|
||||
log_error(QString("Slot size") +
|
||||
QString::number(output_pool->slot_data_bytes()));
|
||||
*response = error_message(
|
||||
QStringLiteral("rendered frame does not fit output slot "),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::memcpy(output_pool->slot_data(slot), frame->const_data(),
|
||||
size_t(data_size));
|
||||
olive::ipc::FrameSlotMeta *meta = output_pool->meta(slot);
|
||||
meta->id = message.ticket_id;
|
||||
meta->time_num = frame->timestamp().numerator();
|
||||
meta->time_den = frame->timestamp().denominator();
|
||||
meta->width = frame->width();
|
||||
meta->height = frame->height();
|
||||
meta->format = int32_t(frame->format());
|
||||
meta->channel_count = frame->channel_count();
|
||||
meta->linesize = frame->linesize_bytes();
|
||||
meta->data_size = data_size;
|
||||
|
||||
if (!output_pool->publish(slot)) {
|
||||
output_pool->release(slot);
|
||||
*response = error_message(
|
||||
QStringLiteral("failed to publish output frame slot"),
|
||||
message.ticket_id);
|
||||
return true;
|
||||
}
|
||||
olive::ipc::FrameReadyMsg ready;
|
||||
ready.ticket_id = message.ticket_id;
|
||||
ready.output_slot = int(slot);
|
||||
*response = ready.to_json();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::Renderer *create_renderer(const char *backend, bool *valid)
|
||||
{
|
||||
*valid = false;
|
||||
const QString backend_name =
|
||||
backend && *backend ? QString::fromUtf8(backend).toLower() :
|
||||
QStringLiteral("opengl");
|
||||
|
||||
olive::Renderer *renderer;
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
auto *dynamic_renderer = new olive::DynamicRenderer(backend_name);
|
||||
if (dynamic_renderer->init()) {
|
||||
dynamic_renderer->post_init();
|
||||
renderer = dynamic_renderer;
|
||||
} else {
|
||||
delete dynamic_renderer;
|
||||
qWarning() << "Failed to initialize dynamic" << backend_name
|
||||
<< "backend, falling back to direct OpenGL renderer";
|
||||
renderer = new olive::OpenGLRenderer();
|
||||
if (!renderer->init()) {
|
||||
log_error(QStringLiteral("failed to initialize OpenGL renderer"));
|
||||
delete renderer;
|
||||
return nullptr;
|
||||
}
|
||||
renderer->post_init();
|
||||
}
|
||||
#else
|
||||
renderer = new olive::OpenGLRenderer();
|
||||
if (!renderer->Init()) {
|
||||
log_error(QStringLiteral("failed to initialize OpenGL renderer"));
|
||||
delete renderer;
|
||||
return nullptr;
|
||||
}
|
||||
renderer->PostInit();
|
||||
#endif
|
||||
|
||||
// Validate the renderer. For OpenGL we check the GL context; for Vulkan we
|
||||
// rely on init()/post_init() succeeding (there is no QOpenGLContext).
|
||||
bool renderer_valid = true;
|
||||
if (backend_name == QStringLiteral("opengl")) {
|
||||
QOpenGLContext *ctx = nullptr;
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
if (auto *loaded_renderer =
|
||||
dynamic_cast<olive::DynamicRenderer *>(renderer)) {
|
||||
ctx = loaded_renderer->open_gl_context();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ctx = static_cast<olive::OpenGLRenderer *>(renderer)->context();
|
||||
}
|
||||
if (!ctx || !ctx->isValid()) {
|
||||
renderer_valid = false;
|
||||
}
|
||||
}
|
||||
if (!renderer_valid) {
|
||||
log_error(QStringLiteral("OpenGL context is not valid after init"));
|
||||
renderer->destroy();
|
||||
renderer->post_destroy();
|
||||
delete renderer;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
*valid = true;
|
||||
return renderer;
|
||||
}
|
||||
|
||||
bool backend_requests_no_renderer(const char *backend)
|
||||
{
|
||||
if (!backend || !*backend) {
|
||||
return true;
|
||||
}
|
||||
const QString name = QString::fromUtf8(backend).toLower();
|
||||
return name == QStringLiteral("none");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
OakWorkerSession *oakengine_worker_session_create(const char *backend)
|
||||
{
|
||||
auto *session = new (std::nothrow) OakWorkerSession();
|
||||
if (!session) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!backend_requests_no_renderer(backend)) {
|
||||
bool valid = false;
|
||||
session->renderer = create_renderer(backend, &valid);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
void oakengine_worker_session_free(OakWorkerSession *self)
|
||||
{
|
||||
delete self;
|
||||
}
|
||||
|
||||
int oakengine_worker_session_has_renderer(const OakWorkerSession *self)
|
||||
{
|
||||
return self && self->renderer ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_worker_session_initialize_runtime(OakWorkerSession *self)
|
||||
{
|
||||
if (!self) {
|
||||
return 0;
|
||||
}
|
||||
return self->initialize_runtime() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_worker_session_startup_handshake(OakWorkerSession *self,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return -1;
|
||||
}
|
||||
const QByteArray json = QJsonDocument(self->startup_handshake())
|
||||
.toJson(QJsonDocument::Compact);
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = std::min(int(json.size()), buf_size - 1);
|
||||
std::memcpy(buf, json.constData(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return int(json.size());
|
||||
}
|
||||
|
||||
int oakengine_worker_session_handle_json(OakWorkerSession *self,
|
||||
const char *line, char *response_buf,
|
||||
int response_buf_size)
|
||||
{
|
||||
if (!self || !line) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
QJsonParseError parse_error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(QByteArray(line),
|
||||
&parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
const QJsonObject response =
|
||||
error_message(QStringLiteral("malformed control message"));
|
||||
const QByteArray json =
|
||||
QJsonDocument(response).toJson(QJsonDocument::Compact);
|
||||
if (response_buf && response_buf_size > 0) {
|
||||
const int n = std::min(int(json.size()), response_buf_size - 1);
|
||||
std::memcpy(response_buf, json.constData(), size_t(n));
|
||||
response_buf[n] = '\0';
|
||||
}
|
||||
return int(json.size());
|
||||
}
|
||||
|
||||
QJsonObject response;
|
||||
if (!self->handle(doc.object(), &response)) {
|
||||
return -1;
|
||||
}
|
||||
if (response.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const QByteArray json =
|
||||
QJsonDocument(response).toJson(QJsonDocument::Compact);
|
||||
if (response_buf && response_buf_size > 0) {
|
||||
const int n = std::min(int(json.size()), response_buf_size - 1);
|
||||
std::memcpy(response_buf, json.constData(), size_t(n));
|
||||
response_buf[n] = '\0';
|
||||
}
|
||||
return int(json.size());
|
||||
}
|
||||
|
||||
int oakengine_worker_session_shutdown_requested(const OakWorkerSession *self)
|
||||
{
|
||||
return self && self->shutdown_requested ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_worker_main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
|
||||
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
|
||||
install_surface_format();
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
HideWorkerDockIcon();
|
||||
#endif
|
||||
|
||||
QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org"));
|
||||
QCoreApplication::setApplicationName(QStringLiteral("oak-render-worker"));
|
||||
|
||||
QString backend = QStringLiteral("opengl");
|
||||
const QStringList args = app.arguments();
|
||||
for (int i = 1; i < args.size(); ++i) {
|
||||
if (args[i] == QStringLiteral("--backend") && i + 1 < args.size()) {
|
||||
backend = args[i + 1].toLower();
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
std::signal(SIGSEGV, print_backtrace);
|
||||
std::signal(SIGABRT, print_backtrace);
|
||||
std::signal(SIGFPE, print_backtrace);
|
||||
#endif
|
||||
|
||||
QFile in;
|
||||
QFile out;
|
||||
if (!in.open(stdin, QIODevice::ReadOnly | QIODevice::Unbuffered) ||
|
||||
!out.open(stdout, QIODevice::WriteOnly | QIODevice::Unbuffered)) {
|
||||
log_error(QStringLiteral("failed to open stdio control pipes"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
OakWorkerSession *worker =
|
||||
oakengine_worker_session_create(backend.toUtf8().constData());
|
||||
if (!worker || !worker->renderer) {
|
||||
oakengine_worker_session_free(worker);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int exit_code = 0;
|
||||
if (!worker->initialize_runtime()) {
|
||||
exit_code = 1;
|
||||
} else {
|
||||
const QJsonObject handshake = worker->startup_handshake();
|
||||
if (!olive::ipc::write_message(&out, handshake)) {
|
||||
exit_code = 1;
|
||||
} else {
|
||||
out.flush();
|
||||
QByteArray buffer;
|
||||
while (!worker->shutdown_requested && !in.atEnd()) {
|
||||
const QByteArray chunk = in.readLine();
|
||||
if (chunk.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.append(chunk);
|
||||
while (true) {
|
||||
QJsonObject message;
|
||||
bool ok = true;
|
||||
if (!olive::ipc::read_message(&buffer, &message, &ok)) {
|
||||
if (!ok) {
|
||||
olive::ipc::write_message(
|
||||
&out, error_message(QStringLiteral(
|
||||
"malformed control message")));
|
||||
out.flush();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
QJsonObject response;
|
||||
if (!worker->handle(message, &response)) {
|
||||
exit_code = 1;
|
||||
break;
|
||||
}
|
||||
if (!response.isEmpty()) {
|
||||
olive::ipc::write_message(&out, response);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oakengine_worker_session_free(worker);
|
||||
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,510 @@
|
||||
/***
|
||||
|
||||
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 application facade
|
||||
// (oakengine/app.h). Exercises the CoreParams startup, the start/stop state
|
||||
// machine, the tool/snapping/timecode state with its change notifications,
|
||||
// the recent-projects list, the status bar, the clipboard, the footage
|
||||
// filter and the project lifecycle. No GPU: everything runs on the
|
||||
// offscreen QGuiApplication created by the facade itself.
|
||||
//
|
||||
// Not covered (they require a running import/load task or the autorecovery
|
||||
// timer, which need an event loop): the confirm_image_sequence,
|
||||
// relink_footage, save_project and load_layout handler invocations.
|
||||
// Registration of every handler field is exercised and the close_project
|
||||
// handler is verified through oakengine_app_create_new_project().
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "oakengine/app.h"
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/project.h"
|
||||
|
||||
// Recording sink for every OakEngineAppCallbacks field.
|
||||
typedef struct {
|
||||
int confirm_image_sequence_calls;
|
||||
int relink_calls;
|
||||
int save_project_calls;
|
||||
int close_project_calls;
|
||||
int close_project_ret;
|
||||
int load_layout_calls;
|
||||
int otio_import_calls;
|
||||
int status_show_calls;
|
||||
char last_status[256];
|
||||
int last_timeout;
|
||||
int status_clear_calls;
|
||||
int cache_full_calls;
|
||||
int active_project_calls;
|
||||
OakEngineProject *last_project;
|
||||
int tool_changed_calls;
|
||||
int last_tool;
|
||||
int addable_changed_calls;
|
||||
int last_addable;
|
||||
int snapping_changed_calls;
|
||||
int last_snapping;
|
||||
int timecode_changed_calls;
|
||||
int last_display;
|
||||
int recent_changed_calls;
|
||||
int color_picker_calls;
|
||||
int last_color_picker;
|
||||
} Cb;
|
||||
|
||||
static Cb g_cb;
|
||||
|
||||
static int on_confirm_image_sequence(const char *filename, void *userdata)
|
||||
{
|
||||
(void) filename;
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.confirm_image_sequence_calls++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int on_relink_footage(OakEngineFootage **footage, int count,
|
||||
void *userdata)
|
||||
{
|
||||
(void) footage;
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.relink_calls++;
|
||||
assert(count >= 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void on_save_project(const char *override_filename, void *userdata)
|
||||
{
|
||||
(void) override_filename;
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.save_project_calls++;
|
||||
}
|
||||
|
||||
static int on_close_project(void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.close_project_calls++;
|
||||
// Mirror the application's close: detach and delete the open project
|
||||
OakEngineProject *p = oakengine_app_open_project();
|
||||
if (p) {
|
||||
oakengine_app_set_active_project(NULL);
|
||||
oakengine_project_free(p);
|
||||
}
|
||||
return g_cb.close_project_ret;
|
||||
}
|
||||
|
||||
static void on_load_layout(const void *layout, void *userdata)
|
||||
{
|
||||
(void) layout;
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.load_layout_calls++;
|
||||
}
|
||||
|
||||
static int on_otio_import(OakEngineSequence **sequences, int count,
|
||||
void *userdata)
|
||||
{
|
||||
(void) sequences;
|
||||
(void) count;
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.otio_import_calls++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void on_status_message_show(const char *message, int timeout,
|
||||
void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.status_show_calls++;
|
||||
snprintf(g_cb.last_status, sizeof(g_cb.last_status), "%s", message);
|
||||
g_cb.last_timeout = timeout;
|
||||
}
|
||||
|
||||
static void on_status_message_clear(void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.status_clear_calls++;
|
||||
}
|
||||
|
||||
static void on_cache_full_warning(void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.cache_full_calls++;
|
||||
}
|
||||
|
||||
static void on_active_project_changed(OakEngineProject *project,
|
||||
void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.active_project_calls++;
|
||||
g_cb.last_project = project;
|
||||
}
|
||||
|
||||
static void on_tool_changed(int tool, void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.tool_changed_calls++;
|
||||
g_cb.last_tool = tool;
|
||||
}
|
||||
|
||||
static void on_addable_object_changed(int object, void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.addable_changed_calls++;
|
||||
g_cb.last_addable = object;
|
||||
}
|
||||
|
||||
static void on_snapping_changed(int snapping, void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.snapping_changed_calls++;
|
||||
g_cb.last_snapping = snapping;
|
||||
}
|
||||
|
||||
static void on_timecode_display_changed(int display, void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.timecode_changed_calls++;
|
||||
g_cb.last_display = display;
|
||||
}
|
||||
|
||||
static void on_open_recent_list_changed(void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.recent_changed_calls++;
|
||||
}
|
||||
|
||||
static void on_color_picker_enabled(int enabled, void *userdata)
|
||||
{
|
||||
assert(userdata == &g_cb);
|
||||
g_cb.color_picker_calls++;
|
||||
g_cb.last_color_picker = enabled;
|
||||
}
|
||||
|
||||
static OakEngineAppCallbacks make_callbacks(void)
|
||||
{
|
||||
OakEngineAppCallbacks cb = { 0 };
|
||||
cb.userdata = &g_cb;
|
||||
cb.confirm_image_sequence = on_confirm_image_sequence;
|
||||
cb.relink_footage = on_relink_footage;
|
||||
cb.save_project = on_save_project;
|
||||
cb.close_project = on_close_project;
|
||||
cb.load_layout = on_load_layout;
|
||||
cb.otio_import = on_otio_import;
|
||||
cb.status_message_show = on_status_message_show;
|
||||
cb.status_message_clear = on_status_message_clear;
|
||||
cb.cache_full_warning = on_cache_full_warning;
|
||||
cb.active_project_changed = on_active_project_changed;
|
||||
cb.tool_changed = on_tool_changed;
|
||||
cb.addable_object_changed = on_addable_object_changed;
|
||||
cb.snapping_changed = on_snapping_changed;
|
||||
cb.timecode_display_changed = on_timecode_display_changed;
|
||||
cb.open_recent_list_changed = on_open_recent_list_changed;
|
||||
cb.color_picker_enabled = on_color_picker_enabled;
|
||||
return cb;
|
||||
}
|
||||
|
||||
// Query a buf/size string function into a heap buffer (caller frees).
|
||||
static char *query0(int (*fn)(char *, int))
|
||||
{
|
||||
const int needed = fn(NULL, 0);
|
||||
assert(needed >= 0);
|
||||
char *buf = (char *) malloc(size_t(needed) + 1);
|
||||
assert(fn(buf, needed + 1) == needed);
|
||||
buf[needed] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void test_create_and_params(void)
|
||||
{
|
||||
OakEngineAppParams params = { 0 };
|
||||
params.run_mode = OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE;
|
||||
params.fullscreen = 1;
|
||||
params.startup_project = "/tmp/startup.ove";
|
||||
|
||||
// NULL params would be valid too, but verify the values round-trip
|
||||
assert(oakengine_app_create(¶ms) == OAKENGINE_OK);
|
||||
assert(oakengine_app_run_mode() == OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE);
|
||||
assert(oakengine_app_fullscreen() == 1);
|
||||
char *startup = query0(oakengine_app_startup_project);
|
||||
assert(strcmp(startup, "/tmp/startup.ove") == 0);
|
||||
free(startup);
|
||||
|
||||
// Only one application core may exist
|
||||
assert(oakengine_app_create(NULL) == OAKENGINE_E_STATE);
|
||||
}
|
||||
|
||||
static void test_start_stop(void)
|
||||
{
|
||||
// Not started yet
|
||||
assert(oakengine_app_stop() == OAKENGINE_E_STATE);
|
||||
|
||||
// Full engine start (config, managers, autorecovery, recent list)
|
||||
assert(oakengine_app_start() == OAKENGINE_OK);
|
||||
assert(oakengine_app_start() == OAKENGINE_E_STATE);
|
||||
|
||||
assert(oakengine_app_stop() == OAKENGINE_OK);
|
||||
assert(oakengine_app_stop() == OAKENGINE_E_STATE);
|
||||
}
|
||||
|
||||
static void test_tool_state(void)
|
||||
{
|
||||
OakEngineAppCallbacks cb = make_callbacks();
|
||||
assert(oakengine_app_set_callbacks(&cb) == OAKENGINE_OK);
|
||||
|
||||
// Tool (k_none=0 .. k_track_select=13, k_count=14)
|
||||
assert(oakengine_app_set_tool(4) == OAKENGINE_OK);
|
||||
assert(oakengine_app_tool() == 4);
|
||||
assert(g_cb.tool_changed_calls == 1 && g_cb.last_tool == 4);
|
||||
assert(oakengine_app_set_tool(-1) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_app_set_tool(999) == OAKENGINE_E_INVALID);
|
||||
|
||||
// Addable object
|
||||
assert(oakengine_app_set_addable_object(2) == OAKENGINE_OK);
|
||||
assert(oakengine_app_addable_object() == 2);
|
||||
assert(g_cb.addable_changed_calls == 1 && g_cb.last_addable == 2);
|
||||
assert(oakengine_app_set_addable_object(999) == OAKENGINE_E_INVALID);
|
||||
|
||||
// Snapping
|
||||
assert(oakengine_app_set_snapping(0) == OAKENGINE_OK);
|
||||
assert(oakengine_app_snapping() == 0);
|
||||
assert(g_cb.snapping_changed_calls == 1 && g_cb.last_snapping == 0);
|
||||
assert(oakengine_app_set_snapping(1) == OAKENGINE_OK);
|
||||
assert(oakengine_app_snapping() == 1);
|
||||
assert(g_cb.snapping_changed_calls == 2 && g_cb.last_snapping == 1);
|
||||
|
||||
// Timecode display (0..4)
|
||||
assert(oakengine_app_set_timecode_display(3) == OAKENGINE_OK);
|
||||
assert(oakengine_app_timecode_display() == 3);
|
||||
assert(g_cb.timecode_changed_calls == 1 && g_cb.last_display == 3);
|
||||
assert(oakengine_app_set_timecode_display(999) == OAKENGINE_E_INVALID);
|
||||
|
||||
// Selected transition
|
||||
assert(oakengine_app_set_selected_transition("crossdissolve") ==
|
||||
OAKENGINE_OK);
|
||||
char *transition = query0(oakengine_app_selected_transition);
|
||||
assert(strcmp(transition, "crossdissolve") == 0);
|
||||
free(transition);
|
||||
assert(oakengine_app_set_selected_transition(NULL) == OAKENGINE_OK);
|
||||
transition = query0(oakengine_app_selected_transition);
|
||||
assert(transition[0] == '\0');
|
||||
free(transition);
|
||||
|
||||
// Magic flag
|
||||
assert(oakengine_app_set_magic(1) == OAKENGINE_OK);
|
||||
assert(oakengine_app_is_magic_enabled() == 1);
|
||||
assert(oakengine_app_set_magic(0) == OAKENGINE_OK);
|
||||
assert(oakengine_app_is_magic_enabled() == 0);
|
||||
}
|
||||
|
||||
static void test_status_and_pixel_sampling(void)
|
||||
{
|
||||
assert(oakengine_app_show_status_message("hello", 250) == OAKENGINE_OK);
|
||||
assert(g_cb.status_show_calls == 1);
|
||||
assert(strcmp(g_cb.last_status, "hello") == 0);
|
||||
assert(g_cb.last_timeout == 250);
|
||||
assert(oakengine_app_show_status_message(NULL, 0) == OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_app_clear_status_message() == OAKENGINE_OK);
|
||||
assert(g_cb.status_clear_calls == 1);
|
||||
|
||||
// Pixel sampling ref-count emits only when crossing zero
|
||||
assert(oakengine_app_request_pixel_sampling(1) == OAKENGINE_OK);
|
||||
assert(oakengine_app_request_pixel_sampling(1) == OAKENGINE_OK);
|
||||
assert(g_cb.color_picker_calls == 1 && g_cb.last_color_picker == 1);
|
||||
assert(oakengine_app_request_pixel_sampling(0) == OAKENGINE_OK);
|
||||
assert(g_cb.color_picker_calls == 1);
|
||||
assert(oakengine_app_request_pixel_sampling(0) == OAKENGINE_OK);
|
||||
assert(g_cb.color_picker_calls == 2 && g_cb.last_color_picker == 0);
|
||||
}
|
||||
|
||||
static void test_project_lifecycle(void)
|
||||
{
|
||||
g_cb.close_project_ret = 1;
|
||||
const int active_before = g_cb.active_project_calls;
|
||||
|
||||
// New project goes through the close handler and becomes active
|
||||
assert(oakengine_app_create_new_project() == OAKENGINE_OK);
|
||||
assert(g_cb.close_project_calls == 1);
|
||||
OakEngineProject *p = oakengine_app_open_project();
|
||||
assert(p != NULL);
|
||||
assert(g_cb.active_project_calls > active_before);
|
||||
assert(g_cb.last_project == p);
|
||||
|
||||
// Replacing it invokes the close handler again
|
||||
assert(oakengine_app_create_new_project() == OAKENGINE_OK);
|
||||
assert(g_cb.close_project_calls == 2);
|
||||
p = oakengine_app_open_project();
|
||||
assert(p != NULL);
|
||||
|
||||
// Saving a project without a filename keeps the recent list unchanged
|
||||
assert(oakengine_app_clear_recent_projects() == OAKENGINE_OK);
|
||||
assert(oakengine_app_on_project_saved(p) == OAKENGINE_OK);
|
||||
assert(oakengine_app_recent_projects_count() == 0);
|
||||
|
||||
// Detach and free it again
|
||||
assert(oakengine_app_set_active_project(NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_app_open_project() == NULL);
|
||||
assert(g_cb.last_project == NULL);
|
||||
oakengine_project_free(p);
|
||||
|
||||
// add_open_project adopts an externally created project
|
||||
p = oakengine_project_create();
|
||||
assert(p != NULL);
|
||||
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||
assert(oakengine_app_add_open_project(p, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_app_open_project() == p);
|
||||
assert(oakengine_app_set_active_project(NULL) == OAKENGINE_OK);
|
||||
oakengine_project_free(p);
|
||||
|
||||
// NULL tolerance
|
||||
assert(oakengine_app_add_open_project(NULL, 0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_app_on_project_saved(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_app_add_open_project_from_task(NULL, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_app_add_recovery_project_from_task(NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_recent_projects(void)
|
||||
{
|
||||
// Start from a clean list
|
||||
assert(oakengine_app_clear_recent_projects() == OAKENGINE_OK);
|
||||
assert(oakengine_app_recent_projects_count() == 0);
|
||||
const int changes_before = g_cb.recent_changed_calls;
|
||||
|
||||
// A saved project lands in the recent list through on_project_saved
|
||||
OakEngineProject *p = oakengine_project_create();
|
||||
assert(p != NULL);
|
||||
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||
assert(oakengine_project_save(p, "oakengine_app_test_recent.ove") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_app_on_project_saved(p) == OAKENGINE_OK);
|
||||
assert(oakengine_app_recent_projects_count() == 1);
|
||||
assert(g_cb.recent_changed_calls > changes_before);
|
||||
|
||||
const int needed = oakengine_app_recent_project_at(0, NULL, 0);
|
||||
assert(needed > 0);
|
||||
char *buf = (char *) malloc(size_t(needed) + 1);
|
||||
assert(oakengine_app_recent_project_at(0, buf, needed + 1) == needed);
|
||||
buf[needed] = '\0';
|
||||
assert(strstr(buf, "oakengine_app_test_recent.ove") != NULL);
|
||||
free(buf);
|
||||
|
||||
// Out-of-range access is rejected (the engine would assert otherwise)
|
||||
assert(oakengine_app_recent_project_at(5, NULL, 0) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_app_remove_recent_project(5) == OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
assert(oakengine_app_remove_recent_project(0) == OAKENGINE_OK);
|
||||
assert(oakengine_app_recent_projects_count() == 0);
|
||||
|
||||
remove("oakengine_app_test_recent.ove");
|
||||
oakengine_project_free(p);
|
||||
}
|
||||
|
||||
static void test_clipboard(void)
|
||||
{
|
||||
assert(oakengine_app_copy_to_clipboard("hello clipboard") ==
|
||||
OAKENGINE_OK);
|
||||
char *text = query0(oakengine_app_paste_from_clipboard);
|
||||
assert(strcmp(text, "hello clipboard") == 0);
|
||||
free(text);
|
||||
assert(oakengine_app_copy_to_clipboard(NULL) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_footage_filter(void)
|
||||
{
|
||||
assert(oakengine_app_is_footage_extension_allowed("movie.mp4") == 1);
|
||||
assert(oakengine_app_is_footage_extension_allowed("IMAGE.PNG") == 1);
|
||||
assert(oakengine_app_is_footage_extension_allowed("doc.txt") == 0);
|
||||
assert(oakengine_app_is_footage_extension_allowed(NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
char *filter = query0(oakengine_app_footage_file_dialog_filter);
|
||||
assert(strstr(filter, "*.mp4") != NULL);
|
||||
assert(strstr(filter, ";;") != NULL);
|
||||
free(filter);
|
||||
}
|
||||
|
||||
static void test_misc(void)
|
||||
{
|
||||
// Unknown locale is reported as "not found" without failing
|
||||
assert(oakengine_app_set_language("definitely_not_a_locale_xx") == 0);
|
||||
assert(oakengine_app_set_language(NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_app_set_autorecovery_interval(5) == OAKENGINE_OK);
|
||||
assert(oakengine_app_set_use_proxy_media(1) == OAKENGINE_OK);
|
||||
|
||||
char *index = query0(oakengine_app_auto_recovery_index_filename);
|
||||
assert(strstr(index, "unrecovered") != NULL);
|
||||
free(index);
|
||||
|
||||
assert(oakengine_app_undo_stack() != NULL);
|
||||
}
|
||||
|
||||
static void test_create_sequence(void)
|
||||
{
|
||||
OakEngineProject *p = oakengine_project_create();
|
||||
assert(p != NULL);
|
||||
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||
|
||||
OakEngineSequence *s = oakengine_app_create_sequence(p, "Seq %1");
|
||||
assert(s != NULL);
|
||||
|
||||
assert(oakengine_app_create_sequence(NULL, NULL) == NULL);
|
||||
|
||||
// The returned sequence is not yet part of the project (owned by the
|
||||
// caller); this test intentionally leaves it unparented.
|
||||
oakengine_project_free(p);
|
||||
}
|
||||
|
||||
static void test_callbacks_clear(void)
|
||||
{
|
||||
assert(oakengine_app_set_callbacks(NULL) == OAKENGINE_OK);
|
||||
|
||||
// State changes still work, but nothing is delivered anymore
|
||||
const int calls = g_cb.tool_changed_calls;
|
||||
assert(oakengine_app_set_tool(2) == OAKENGINE_OK);
|
||||
assert(oakengine_app_tool() == 2);
|
||||
assert(g_cb.tool_changed_calls == calls);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
// The facade brings up its own offscreen application object
|
||||
test_create_and_params();
|
||||
test_start_stop();
|
||||
|
||||
// Engine services (incl. renderer manager for set_active_project)
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS | OAKENGINE_INIT_RENDER) ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
test_tool_state();
|
||||
test_status_and_pixel_sampling();
|
||||
test_project_lifecycle();
|
||||
test_recent_projects();
|
||||
test_clipboard();
|
||||
test_footage_filter();
|
||||
test_misc();
|
||||
test_create_sequence();
|
||||
test_callbacks_clear();
|
||||
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/***
|
||||
|
||||
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 audio I/O family
|
||||
// (oakengine/audio.h). Exercises the AudioManager instance lifecycle,
|
||||
// input/output device get/set round-trips, output push error paths and the
|
||||
// output_params_changed event subscription. No GL or QApplication required.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "oakengine/audio.h"
|
||||
#include "oakengine/events.h"
|
||||
#include "oakengine/init.h"
|
||||
|
||||
static int g_output_params_changed_count;
|
||||
static void *g_output_params_changed_source;
|
||||
|
||||
static void on_output_params_changed(const oakengine_event *event, void *)
|
||||
{
|
||||
assert(event != NULL);
|
||||
assert(event->id == OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED);
|
||||
g_output_params_changed_count++;
|
||||
g_output_params_changed_source = event->source;
|
||||
}
|
||||
|
||||
static void test_instance_lifecycle(void)
|
||||
{
|
||||
// No instance before create.
|
||||
assert(oakengine_audio_manager_handle() == NULL);
|
||||
assert(oakengine_audio_get_output_device() == -1);
|
||||
assert(oakengine_audio_get_input_device() == -1);
|
||||
assert(oakengine_audio_clear_buffered_output() == OAKENGINE_E_STATE);
|
||||
assert(oakengine_audio_stop_recording() == OAKENGINE_E_STATE);
|
||||
|
||||
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_audio_manager_handle() != NULL);
|
||||
|
||||
// Idempotent create is allowed.
|
||||
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_audio_manager_handle() != NULL);
|
||||
|
||||
oakengine_audio_destroy_instance();
|
||||
assert(oakengine_audio_manager_handle() == NULL);
|
||||
|
||||
// Idempotent destroy is allowed.
|
||||
assert(oakengine_audio_destroy_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_audio_manager_handle() == NULL);
|
||||
}
|
||||
|
||||
static void test_device_round_trip(void)
|
||||
{
|
||||
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
|
||||
void *handle = oakengine_audio_manager_handle();
|
||||
assert(handle != NULL);
|
||||
|
||||
// Default is usually paNoDevice (-1) in headless environments.
|
||||
const int64_t original_output = oakengine_audio_get_output_device();
|
||||
const int64_t original_input = oakengine_audio_get_input_device();
|
||||
|
||||
// Setting a value should change the returned value.
|
||||
assert(oakengine_audio_set_output_device(42) == OAKENGINE_OK);
|
||||
assert(oakengine_audio_get_output_device() == 42);
|
||||
|
||||
assert(oakengine_audio_set_input_device(43) == OAKENGINE_OK);
|
||||
assert(oakengine_audio_get_input_device() == 43);
|
||||
|
||||
// hard_reset re-initializes PortAudio and should not crash.
|
||||
assert(oakengine_audio_hard_reset() == OAKENGINE_OK);
|
||||
assert(oakengine_audio_manager_handle() == handle);
|
||||
|
||||
// Restore original values.
|
||||
assert(oakengine_audio_set_output_device(original_output) == OAKENGINE_OK);
|
||||
assert(oakengine_audio_set_input_device(original_input) == OAKENGINE_OK);
|
||||
assert(oakengine_audio_get_output_device() == original_output);
|
||||
assert(oakengine_audio_get_input_device() == original_input);
|
||||
|
||||
oakengine_audio_destroy_instance();
|
||||
}
|
||||
|
||||
static void test_push_to_output_errors(void)
|
||||
{
|
||||
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
|
||||
|
||||
char error_buf[256];
|
||||
memset(error_buf, 0, sizeof(error_buf));
|
||||
|
||||
// NULL params is rejected without crashing.
|
||||
assert(oakengine_audio_push_to_output(NULL, "x", 1, error_buf,
|
||||
sizeof(error_buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// NULL samples is rejected.
|
||||
assert(oakengine_audio_push_to_output((const OakAudioParams *)1, NULL, 1,
|
||||
error_buf, sizeof(error_buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
oakengine_audio_destroy_instance();
|
||||
}
|
||||
|
||||
static void test_output_params_changed_event(void)
|
||||
{
|
||||
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
|
||||
void *handle = oakengine_audio_manager_handle();
|
||||
assert(handle != NULL);
|
||||
|
||||
g_output_params_changed_count = 0;
|
||||
g_output_params_changed_source = NULL;
|
||||
|
||||
const int64_t sub = oakengine_event_subscribe(
|
||||
handle, OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED,
|
||||
on_output_params_changed, NULL);
|
||||
assert(sub > 0);
|
||||
|
||||
const int64_t original_output = oakengine_audio_get_output_device();
|
||||
assert(oakengine_audio_set_output_device(84) == OAKENGINE_OK);
|
||||
assert(g_output_params_changed_count >= 1);
|
||||
assert(g_output_params_changed_source == handle);
|
||||
|
||||
assert(oakengine_event_unsubscribe(sub) == OAKENGINE_OK);
|
||||
|
||||
// No further deliveries after unsubscribe.
|
||||
const int count_after_unsub = g_output_params_changed_count;
|
||||
assert(oakengine_audio_set_output_device(85) == OAKENGINE_OK);
|
||||
assert(g_output_params_changed_count == count_after_unsub);
|
||||
|
||||
// Restore original value.
|
||||
assert(oakengine_audio_set_output_device(original_output) == OAKENGINE_OK);
|
||||
|
||||
oakengine_audio_destroy_instance();
|
||||
}
|
||||
|
||||
static void test_audio_sync_algorithms(void)
|
||||
{
|
||||
// place_by_waveform_offset: a 1-second positive offset at 48 kHz
|
||||
oak_audio_sync_placement placement;
|
||||
assert(oakengine_audio_sync_place_by_waveform_offset(
|
||||
0, 1, 48000, 48000, &placement) == OAKENGINE_OK);
|
||||
assert(placement.valid);
|
||||
assert(placement.timeline_in_num == 1 && placement.timeline_in_den == 1);
|
||||
|
||||
// place_by_source_time: matching source/media in points -> same timeline in
|
||||
oak_audio_sync_source_clip ref = { 0, 1, 0, 1, 1 };
|
||||
oak_audio_sync_source_clip cand = { 0, 1, 0, 1, 1 };
|
||||
assert(oakengine_audio_sync_place_by_source_time(
|
||||
&ref, &cand, 5, 1, &placement) == OAKENGINE_OK);
|
||||
assert(placement.valid);
|
||||
assert(placement.timeline_in_num == 5 && placement.timeline_in_den == 1);
|
||||
|
||||
// estimate_envelope_offset: identical envelopes -> zero offset, high
|
||||
// confidence
|
||||
double envelope[10] = { 0, 1, 2, 3, 4, 5, 4, 3, 2, 1 };
|
||||
oak_audio_waveform_offset offset;
|
||||
assert(oakengine_audio_estimate_envelope_offset(
|
||||
envelope, 10, envelope, 10, NULL, 0, NULL, 0, 1, 5,
|
||||
&offset) == OAKENGINE_OK);
|
||||
assert(offset.valid);
|
||||
assert(offset.offset_samples == 0);
|
||||
assert(offset.confidence > 0.99);
|
||||
|
||||
// estimate_stretch_and_offset: identical envelopes at rate 1 -> zero offset
|
||||
oak_audio_waveform_stretch_offset stretch;
|
||||
assert(oakengine_audio_estimate_stretch_and_offset(
|
||||
envelope, 10, envelope, 10, NULL, 0, NULL, 0, 1, 5, 0.9, 1.1,
|
||||
0.05, &stretch) == OAKENGINE_OK);
|
||||
assert(stretch.valid);
|
||||
assert(stretch.offset_samples == 0);
|
||||
assert(stretch.rate > 0.99 && stretch.rate < 1.01);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_instance_lifecycle();
|
||||
test_device_round_trip();
|
||||
test_push_to_output_errors();
|
||||
test_output_params_changed_event();
|
||||
test_audio_sync_algorithms();
|
||||
|
||||
oakengine_shutdown();
|
||||
|
||||
printf("oakengine_audio_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
/***
|
||||
|
||||
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 color facade (oakengine/color.h)
|
||||
// and the color manager events (oakengine/events.h). Exercises the
|
||||
// manager queries (config filename, colorspace/display/view/look lists,
|
||||
// defaults, luma coefficients, compliant-space resolution), the standalone
|
||||
// config handle, the color processor handle (create/convert/id) and the
|
||||
// event subscriptions. No GPU: everything here runs on the CPU-side OCIO
|
||||
// wrappers. When the environment provides no usable OCIO config at all
|
||||
// (colorspace count 0), the query assertions are skipped but the
|
||||
// robustness checks (NULL handling, error paths) still run.
|
||||
|
||||
#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/color.h"
|
||||
#include "oakengine/events.h"
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/project.h"
|
||||
|
||||
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_color_test_%lu", base,
|
||||
(unsigned long)GetCurrentProcessId());
|
||||
assert(_mkdir(g_tmpdir) == 0);
|
||||
#else
|
||||
strcpy(g_tmpdir, "/tmp/oakengine_color_test_XXXXXX");
|
||||
assert(mkdtemp(g_tmpdir) != NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- Robustness: NULL/invalid arguments ------------------------------------
|
||||
|
||||
static void test_null_robustness(void)
|
||||
{
|
||||
char buf[64];
|
||||
double rgb[3];
|
||||
double rgba[4] = { 0, 0, 0, 0 };
|
||||
|
||||
assert(oakengine_color_manager_from_project(NULL) == NULL);
|
||||
assert(oakengine_color_manager_get_config_filename(NULL, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_set_config_filename(NULL, "x") ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_colorspace_count(NULL) == 0);
|
||||
assert(oakengine_color_manager_colorspace_at(NULL, 0, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_display_count(NULL) == 0);
|
||||
assert(oakengine_color_manager_display_at(NULL, 0, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_view_count(NULL, NULL) == 0);
|
||||
assert(oakengine_color_manager_view_at(NULL, NULL, 0, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_look_count(NULL) == 0);
|
||||
assert(oakengine_color_manager_look_at(NULL, 0, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_default_display(NULL, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_default_view(NULL, NULL, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_default_input_color_space(NULL, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_set_default_input_color_space(NULL, "x") ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_reference_color_space(NULL, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_default_luma_coefs(NULL, rgb) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_compliant_color_space(NULL, "x", buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_manager_compliant_transform(NULL, NULL, 0, NULL,
|
||||
NULL, 0, NULL, 0, NULL,
|
||||
0) == OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_color_config_load_file(NULL) == NULL);
|
||||
assert(oakengine_color_config_colorspace_count(NULL) == 0);
|
||||
assert(oakengine_color_config_colorspace_at(NULL, 0, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
oakengine_color_config_free(NULL); // no-op
|
||||
|
||||
assert(oakengine_color_processor_create(NULL, "in", NULL,
|
||||
OAKENGINE_COLOR_PROCESSOR_NORMAL) ==
|
||||
NULL);
|
||||
assert(oakengine_color_processor_is_valid(NULL) == 0);
|
||||
assert(oakengine_color_processor_convert_color(NULL, rgba, rgba) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_processor_id(NULL, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
oakengine_color_processor_free(NULL); // no-op
|
||||
}
|
||||
|
||||
// ---- Standalone config handle -----------------------------------------------
|
||||
|
||||
static void test_config_handle(int have_ocio)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
// A missing file must fail cleanly with an error message.
|
||||
assert(oakengine_color_config_load_file("/nonexistent/definitely.ocio") ==
|
||||
NULL);
|
||||
assert(oakengine_color_last_error(buf, sizeof(buf)) > 0);
|
||||
|
||||
OakEngineColorConfig *config = oakengine_color_config_load_default();
|
||||
if (!have_ocio) {
|
||||
// No usable OCIO config in this environment.
|
||||
if (config) {
|
||||
oakengine_color_config_free(config);
|
||||
}
|
||||
return;
|
||||
}
|
||||
assert(config != NULL);
|
||||
|
||||
const int count = oakengine_color_config_colorspace_count(config);
|
||||
assert(count > 0);
|
||||
for (int i = 0; i < count; i++) {
|
||||
assert(oakengine_color_config_colorspace_at(config, i, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(buf[0] != '\0');
|
||||
}
|
||||
assert(oakengine_color_config_colorspace_at(config, count, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_color_config_colorspace_at(config, -1, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
oakengine_color_config_free(config);
|
||||
}
|
||||
|
||||
// ---- Manager queries ---------------------------------------------------------
|
||||
|
||||
static void test_manager_queries(OakEngineColorManager *mgr)
|
||||
{
|
||||
char buf[256];
|
||||
char first_cs[256];
|
||||
double rgb[3] = { 0, 0, 0 };
|
||||
|
||||
// Colorspaces
|
||||
const int cs_count = oakengine_color_manager_colorspace_count(mgr);
|
||||
assert(cs_count > 0);
|
||||
assert(oakengine_color_manager_colorspace_at(mgr, 0, first_cs,
|
||||
sizeof(first_cs)) > 0);
|
||||
assert(first_cs[0] != '\0');
|
||||
assert(oakengine_color_manager_colorspace_at(mgr, cs_count, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Displays / views / looks
|
||||
const int disp_count = oakengine_color_manager_display_count(mgr);
|
||||
assert(disp_count > 0);
|
||||
assert(oakengine_color_manager_display_at(mgr, 0, buf, sizeof(buf)) > 0);
|
||||
char display[256];
|
||||
memcpy(display, buf, sizeof(display));
|
||||
const int view_count = oakengine_color_manager_view_count(mgr, display);
|
||||
assert(view_count > 0);
|
||||
assert(oakengine_color_manager_view_at(mgr, display, 0, buf, sizeof(buf)) >
|
||||
0);
|
||||
assert(oakengine_color_manager_look_count(mgr) >= 0);
|
||||
assert(oakengine_color_manager_display_at(mgr, disp_count, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Defaults
|
||||
char default_display[256];
|
||||
assert(oakengine_color_manager_default_display(mgr, default_display,
|
||||
sizeof(default_display)) > 0);
|
||||
assert(oakengine_color_manager_default_view(mgr, default_display, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(oakengine_color_manager_default_input_color_space(mgr, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(oakengine_color_manager_reference_color_space(mgr, buf,
|
||||
sizeof(buf)) > 0);
|
||||
|
||||
// Default input colorspace set/get roundtrip
|
||||
assert(oakengine_color_manager_set_default_input_color_space(mgr,
|
||||
first_cs) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_color_manager_default_input_color_space(mgr, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, first_cs) == 0);
|
||||
|
||||
// Config filename set/get roundtrip (an empty filename selects the
|
||||
// built-in default config; setting it must not crash the queries above)
|
||||
assert(oakengine_color_manager_set_config_filename(mgr, "") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_color_manager_get_config_filename(mgr, buf, sizeof(buf)) >=
|
||||
0);
|
||||
|
||||
// Luma coefficients: Rec.709-style weights, all positive, roughly sum to 1
|
||||
assert(oakengine_color_manager_default_luma_coefs(mgr, rgb) ==
|
||||
OAKENGINE_OK);
|
||||
assert(rgb[0] > 0 && rgb[1] > 0 && rgb[2] > 0);
|
||||
assert(fabs(rgb[0] + rgb[1] + rgb[2] - 1.0) < 0.01);
|
||||
|
||||
// Compliant colorspace: an existing space resolves to itself, an empty
|
||||
// name resolves to the default input space
|
||||
assert(oakengine_color_manager_compliant_color_space(mgr, first_cs, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, first_cs) == 0);
|
||||
assert(oakengine_color_manager_compliant_color_space(mgr, "", buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, first_cs) == 0);
|
||||
|
||||
// Compliant transform: force a colorspace transform onto a display
|
||||
// transform and back
|
||||
oak_color_transform in;
|
||||
in.is_display = 0;
|
||||
in.output = first_cs;
|
||||
in.view = NULL;
|
||||
in.look = NULL;
|
||||
int out_is_display = -1;
|
||||
char out_o[256], out_v[256], out_l[256];
|
||||
out_o[0] = out_v[0] = out_l[0] = '\0';
|
||||
assert(oakengine_color_manager_compliant_transform(mgr, &in, 1,
|
||||
&out_is_display, out_o,
|
||||
sizeof(out_o), out_v,
|
||||
sizeof(out_v), out_l,
|
||||
sizeof(out_l)) ==
|
||||
OAKENGINE_OK);
|
||||
assert(out_is_display == 1);
|
||||
assert(out_o[0] != '\0');
|
||||
assert(oakengine_color_manager_compliant_transform(
|
||||
mgr, &in, 0, &out_is_display, out_o, sizeof(out_o), out_v,
|
||||
sizeof(out_v), out_l, sizeof(out_l)) == OAKENGINE_OK);
|
||||
assert(out_is_display == 0);
|
||||
assert(strcmp(out_o, first_cs) == 0);
|
||||
}
|
||||
|
||||
// ---- Color processor ----------------------------------------------------------
|
||||
|
||||
static void test_processor(OakEngineColorManager *mgr)
|
||||
{
|
||||
char ref[256];
|
||||
char buf[256];
|
||||
|
||||
assert(oakengine_color_manager_reference_color_space(mgr, ref,
|
||||
sizeof(ref)) > 0);
|
||||
|
||||
// Identity transform (ref -> ref): white stays white
|
||||
oak_color_transform dest;
|
||||
dest.is_display = 0;
|
||||
dest.output = ref;
|
||||
dest.view = NULL;
|
||||
dest.look = NULL;
|
||||
|
||||
OakEngineColorProcessor *proc = oakengine_color_processor_create(
|
||||
mgr, ref, &dest, OAKENGINE_COLOR_PROCESSOR_NORMAL);
|
||||
assert(proc != NULL);
|
||||
assert(oakengine_color_processor_is_valid(proc) == 1);
|
||||
|
||||
const double in[4] = { 1.0, 1.0, 1.0, 1.0 };
|
||||
double out[4] = { 0, 0, 0, 0 };
|
||||
assert(oakengine_color_processor_convert_color(proc, in, out) ==
|
||||
OAKENGINE_OK);
|
||||
assert(fabs(out[0] - 1.0) < 1e-3 && fabs(out[1] - 1.0) < 1e-3 &&
|
||||
fabs(out[2] - 1.0) < 1e-3 && fabs(out[3] - 1.0) < 1e-3);
|
||||
|
||||
// Cache id is non-empty and stable
|
||||
const int id_len = oakengine_color_processor_id(proc, buf, sizeof(buf));
|
||||
assert(id_len > 0);
|
||||
assert(buf[0] != '\0');
|
||||
assert(oakengine_color_processor_id(proc, NULL, 0) == id_len);
|
||||
|
||||
// Inverse direction constructs too
|
||||
OakEngineColorProcessor *inv = oakengine_color_processor_create(
|
||||
mgr, ref, &dest, OAKENGINE_COLOR_PROCESSOR_INVERSE);
|
||||
assert(inv != NULL);
|
||||
oakengine_color_processor_free(inv);
|
||||
|
||||
// Unknown direction is rejected
|
||||
assert(oakengine_color_processor_create(mgr, ref, &dest, 7) == NULL);
|
||||
|
||||
// An unknown colorspace yields an invalid (pass-through) processor,
|
||||
// matching the engine's non-throwing C++ behavior
|
||||
dest.output = "definitely-not-a-colorspace";
|
||||
OakEngineColorProcessor *bad = oakengine_color_processor_create(
|
||||
mgr, ref, &dest, OAKENGINE_COLOR_PROCESSOR_NORMAL);
|
||||
assert(bad != NULL);
|
||||
if (oakengine_color_processor_is_valid(bad)) {
|
||||
// Some configs resolve unknown names via roles; then conversion must
|
||||
// still not crash.
|
||||
assert(oakengine_color_processor_convert_color(bad, in, out) ==
|
||||
OAKENGINE_OK);
|
||||
} else {
|
||||
out[0] = out[1] = out[2] = out[3] = 0;
|
||||
assert(oakengine_color_processor_convert_color(bad, in, out) ==
|
||||
OAKENGINE_OK);
|
||||
assert(out[0] == 1.0 && out[1] == 1.0 && out[2] == 1.0 &&
|
||||
out[3] == 1.0);
|
||||
}
|
||||
oakengine_color_processor_free(bad);
|
||||
|
||||
// Processor is valid and usable.
|
||||
assert(proc != NULL);
|
||||
|
||||
oakengine_color_processor_free(proc);
|
||||
}
|
||||
|
||||
// ---- Events --------------------------------------------------------------------
|
||||
|
||||
static int g_config_events = 0;
|
||||
static int g_reference_events = 0;
|
||||
|
||||
static void count_color_events(const oakengine_event *event, void *userdata)
|
||||
{
|
||||
(void)userdata;
|
||||
assert(event != NULL);
|
||||
if (event->id == OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED) {
|
||||
g_config_events++;
|
||||
} else if (event->id == OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED) {
|
||||
g_reference_events++;
|
||||
} else {
|
||||
assert(0); // unexpected event id on this subscription
|
||||
}
|
||||
}
|
||||
|
||||
static void test_events(OakEngineProject *project,
|
||||
OakEngineColorManager *mgr)
|
||||
{
|
||||
// Family mismatch: a color manager event on a project handle must fail.
|
||||
assert(oakengine_event_subscribe(
|
||||
project, OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED,
|
||||
count_color_events, NULL) == 0);
|
||||
|
||||
int64_t sub_ref = oakengine_event_subscribe(
|
||||
mgr, OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED,
|
||||
count_color_events, NULL);
|
||||
int64_t sub_cfg = oakengine_event_subscribe(
|
||||
mgr, OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED,
|
||||
count_color_events, NULL);
|
||||
assert(sub_ref > 0);
|
||||
assert(sub_cfg > 0);
|
||||
|
||||
// Changing the reference space emits reference_space_changed.
|
||||
char ref[256];
|
||||
assert(oakengine_project_get_color_reference_space(project, ref,
|
||||
sizeof(ref)) > 0);
|
||||
assert(oakengine_project_set_color_reference_space(
|
||||
project, strcmp(ref, "scene_linear") == 0 ? "reference" :
|
||||
"scene_linear") ==
|
||||
OAKENGINE_OK);
|
||||
assert(g_reference_events == 1);
|
||||
assert(g_config_events == 0);
|
||||
|
||||
assert(oakengine_event_unsubscribe(sub_ref) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_cfg) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
|
||||
// Sandbox the config/cache/data locations (the default OCIO config is
|
||||
// extracted under the cache location).
|
||||
#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
|
||||
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_null_robustness();
|
||||
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
OakEngineColorManager *mgr = oakengine_color_manager_from_project(project);
|
||||
assert(mgr != NULL);
|
||||
|
||||
// The built-in default config should always be available (it is
|
||||
// extracted from the engine's resources); tolerate environments where
|
||||
// it is not by skipping the query assertions.
|
||||
const int have_ocio = oakengine_color_manager_colorspace_count(mgr) > 0;
|
||||
if (!have_ocio) {
|
||||
printf("oakengine_color_test: no OCIO config available, skipping "
|
||||
"query tests\n");
|
||||
}
|
||||
|
||||
test_config_handle(have_ocio);
|
||||
if (have_ocio) {
|
||||
test_manager_queries(mgr);
|
||||
test_processor(mgr);
|
||||
}
|
||||
test_events(project, mgr);
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
printf("oakengine_color_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/***
|
||||
|
||||
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 tests for the liboakengine configuration facade
|
||||
// (oakengine/config.h). Runs headless; no GPU required.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "oakengine/config.h"
|
||||
#include "oakengine/init.h"
|
||||
|
||||
static int g_error_calls = 0;
|
||||
static char g_last_title[256];
|
||||
static char g_last_message[256];
|
||||
|
||||
static void error_cb(const char *title, const char *message, void *userdata)
|
||||
{
|
||||
(void) userdata;
|
||||
g_error_calls++;
|
||||
strncpy(g_last_title, title, sizeof(g_last_title) - 1);
|
||||
g_last_title[sizeof(g_last_title) - 1] = '\0';
|
||||
strncpy(g_last_message, message, sizeof(g_last_message) - 1);
|
||||
g_last_message[sizeof(g_last_message) - 1] = '\0';
|
||||
}
|
||||
|
||||
static void test_string_round_trip(void)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
// Missing key returns 0 (empty string).
|
||||
assert(oakengine_config_get_string("oak_test_string_key", buf,
|
||||
sizeof(buf)) == 0);
|
||||
|
||||
assert(oakengine_config_set_string("oak_test_string_key",
|
||||
"hello world") == OAKENGINE_OK);
|
||||
int len = oakengine_config_get_string("oak_test_string_key", buf,
|
||||
sizeof(buf));
|
||||
assert(len == int(strlen("hello world")));
|
||||
assert(strcmp(buf, "hello world") == 0);
|
||||
|
||||
// Query length with NULL buffer.
|
||||
assert(oakengine_config_get_string("oak_test_string_key", NULL, 0) == len);
|
||||
|
||||
// NULL key is rejected.
|
||||
assert(oakengine_config_get_string(NULL, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_config_set_string(NULL, "x") == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_int_round_trip(void)
|
||||
{
|
||||
assert(oakengine_config_get_int("oak_test_int_key", 42) == 42);
|
||||
|
||||
assert(oakengine_config_set_int("oak_test_int_key", 12345) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_config_get_int("oak_test_int_key", 0) == 12345);
|
||||
|
||||
assert(oakengine_config_set_int("oak_test_int_key", -7) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_config_get_int("oak_test_int_key", 0) == -7);
|
||||
|
||||
// NULL key returns default.
|
||||
assert(oakengine_config_get_int(NULL, 99) == 99);
|
||||
assert(oakengine_config_set_int(NULL, 1) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_error_handler(void)
|
||||
{
|
||||
g_error_calls = 0;
|
||||
assert(oakengine_config_set_error_handler(error_cb, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
assert(oakengine_config_report_error("Test Title",
|
||||
"Test Message") == OAKENGINE_OK);
|
||||
assert(g_error_calls == 1);
|
||||
assert(strcmp(g_last_title, "Test Title") == 0);
|
||||
assert(strcmp(g_last_message, "Test Message") == 0);
|
||||
|
||||
// Clearing the handler does not crash.
|
||||
assert(oakengine_config_set_error_handler(NULL, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_config_report_error("Ignored", "Ignored") ==
|
||||
OAKENGINE_OK);
|
||||
assert(g_error_calls == 1);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_string_round_trip();
|
||||
test_int_round_trip();
|
||||
test_error_handler();
|
||||
|
||||
assert(oakengine_config_save() == OAKENGINE_OK);
|
||||
assert(oakengine_config_load() == OAKENGINE_OK);
|
||||
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/***
|
||||
|
||||
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 disk cache family
|
||||
// (oakengine/disk.h). Exercises the DiskManager instance lifecycle, default
|
||||
// cache path queries, cache clearing, settings handler dispatch, folder
|
||||
// handle lookup and default path mutation. Runs headless; no GPU required.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "oakengine/disk.h"
|
||||
#include "oakengine/init.h"
|
||||
|
||||
static char g_handler_path[512];
|
||||
static int g_handler_call_count;
|
||||
|
||||
static void reset_handler_state(void)
|
||||
{
|
||||
memset(g_handler_path, 0, sizeof(g_handler_path));
|
||||
g_handler_call_count = 0;
|
||||
}
|
||||
|
||||
static void settings_handler(const char *folder_path, void *parent_window,
|
||||
void *userdata)
|
||||
{
|
||||
(void) parent_window;
|
||||
(void) userdata;
|
||||
assert(folder_path != NULL);
|
||||
assert(strlen(folder_path) > 0);
|
||||
strncpy(g_handler_path, folder_path, sizeof(g_handler_path) - 1);
|
||||
g_handler_path[sizeof(g_handler_path) - 1] = '\0';
|
||||
g_handler_call_count++;
|
||||
}
|
||||
|
||||
static void test_instance_lifecycle(void)
|
||||
{
|
||||
char buf[512];
|
||||
|
||||
// DiskManager is created by oakengine_init(HEADLESS).
|
||||
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) > 0);
|
||||
|
||||
// Destroy is allowed and idempotent.
|
||||
assert(oakengine_disk_destroy_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_STATE);
|
||||
assert(oakengine_disk_destroy_instance() == OAKENGINE_OK);
|
||||
|
||||
// Create recreates the instance.
|
||||
assert(oakengine_disk_create_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) > 0);
|
||||
|
||||
// Create is idempotent.
|
||||
assert(oakengine_disk_create_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) > 0);
|
||||
}
|
||||
|
||||
static void test_default_cache_path(void)
|
||||
{
|
||||
char buf[512];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
|
||||
const int len = oakengine_disk_get_default_cache_path(buf, sizeof(buf));
|
||||
assert(len > 0);
|
||||
assert((int) strlen(buf) == len);
|
||||
assert(strchr(buf, '/') != NULL || strchr(buf, '\\') != NULL);
|
||||
|
||||
// Query length with NULL buffer.
|
||||
assert(oakengine_disk_get_default_cache_path(NULL, 0) == len);
|
||||
}
|
||||
|
||||
static void test_open_folder_handle(void)
|
||||
{
|
||||
char buf[512];
|
||||
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) > 0);
|
||||
|
||||
void *folder = oakengine_disk_get_open_folder(buf);
|
||||
assert(folder != NULL);
|
||||
|
||||
// NULL/empty path returns the default folder handle.
|
||||
void *default_folder = oakengine_disk_get_open_folder(nullptr);
|
||||
assert(default_folder == folder);
|
||||
|
||||
void *empty_folder = oakengine_disk_get_open_folder("");
|
||||
assert(empty_folder == folder);
|
||||
|
||||
// A different path opens a distinct folder.
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp),
|
||||
#if defined(_WIN32)
|
||||
"%s\\oakengine_disk_test_folder_XXXXXX",
|
||||
#else
|
||||
"%s/oakengine_disk_test_folder_XXXXXX",
|
||||
#endif
|
||||
getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp");
|
||||
|
||||
#if defined(_WIN32)
|
||||
char *tmpdir = _mktemp(tmp);
|
||||
assert(tmpdir != NULL);
|
||||
assert(_mkdir(tmpdir) == 0);
|
||||
#else
|
||||
char *tmpdir = mkdtemp(tmp);
|
||||
assert(tmpdir != NULL);
|
||||
#endif
|
||||
|
||||
void *other_folder = oakengine_disk_get_open_folder(tmpdir);
|
||||
assert(other_folder != NULL);
|
||||
assert(other_folder != folder);
|
||||
|
||||
// The same path returns the same handle.
|
||||
void *other_folder_again = oakengine_disk_get_open_folder(tmpdir);
|
||||
assert(other_folder_again == other_folder);
|
||||
|
||||
#if defined(_WIN32)
|
||||
_rmdir(tmpdir);
|
||||
#else
|
||||
rmdir(tmpdir);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void test_clear_cache(void)
|
||||
{
|
||||
// Create a temporary cache directory and seed it with a file.
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path),
|
||||
#if defined(_WIN32)
|
||||
"%s\\oakengine_disk_test_cache_XXXXXX",
|
||||
#else
|
||||
"%s/oakengine_disk_test_cache_XXXXXX",
|
||||
#endif
|
||||
getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp");
|
||||
|
||||
#if defined(_WIN32)
|
||||
char *tmpdir = _mktemp(path);
|
||||
assert(tmpdir != NULL);
|
||||
assert(_mkdir(tmpdir) == 0);
|
||||
#else
|
||||
char *tmpdir = mkdtemp(path);
|
||||
assert(tmpdir != NULL);
|
||||
#endif
|
||||
|
||||
char index_file[512];
|
||||
snprintf(index_file, sizeof(index_file),
|
||||
#if defined(_WIN32)
|
||||
"%s\\index", tmpdir);
|
||||
#else
|
||||
"%s/index", tmpdir);
|
||||
#endif
|
||||
|
||||
FILE *f = fopen(index_file, "w");
|
||||
assert(f != NULL);
|
||||
fclose(f);
|
||||
|
||||
// clear_cache opens the folder and clears its contents.
|
||||
assert(oakengine_disk_clear_cache(tmpdir) == 1);
|
||||
|
||||
// Re-create a file and clear again to ensure idempotency.
|
||||
f = fopen(index_file, "w");
|
||||
assert(f != NULL);
|
||||
fclose(f);
|
||||
assert(oakengine_disk_clear_cache(tmpdir) == 1);
|
||||
|
||||
#if defined(_WIN32)
|
||||
_rmdir(tmpdir);
|
||||
#else
|
||||
rmdir(tmpdir);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void test_settings_handler_round_trip(void)
|
||||
{
|
||||
reset_handler_state();
|
||||
|
||||
assert(oakengine_disk_set_settings_handler(settings_handler, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
// NULL path uses the default folder.
|
||||
assert(oakengine_disk_show_settings_dialog(NULL, NULL) == OAKENGINE_OK);
|
||||
assert(g_handler_call_count == 1);
|
||||
assert(strlen(g_handler_path) > 0);
|
||||
|
||||
// Calling again with a specific path invokes the handler with that path.
|
||||
assert(oakengine_disk_show_settings_dialog(g_handler_path, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(g_handler_call_count == 2);
|
||||
assert(strcmp(g_handler_path, g_handler_path) == 0);
|
||||
|
||||
// Clearing the handler is allowed and results in a logged skip.
|
||||
assert(oakengine_disk_set_settings_handler(NULL, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_disk_show_settings_dialog(g_handler_path, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(g_handler_call_count == 2);
|
||||
}
|
||||
|
||||
static void test_invalidate_project(void)
|
||||
{
|
||||
// No instance returns an error.
|
||||
assert(oakengine_disk_destroy_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_disk_invalidate_project(NULL) == OAKENGINE_E_STATE);
|
||||
|
||||
assert(oakengine_disk_create_instance() == OAKENGINE_OK);
|
||||
|
||||
// NULL project is accepted (signal emitted with null pointer).
|
||||
assert(oakengine_disk_invalidate_project(NULL) == OAKENGINE_OK);
|
||||
|
||||
// Valid project returns OK without crashing.
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_disk_invalidate_project(project) == OAKENGINE_OK);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_set_default_cache_path(void)
|
||||
{
|
||||
char original[512];
|
||||
assert(oakengine_disk_get_default_cache_path(original, sizeof(original)) >
|
||||
0);
|
||||
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp),
|
||||
#if defined(_WIN32)
|
||||
"%s\\oakengine_disk_test_default_XXXXXX",
|
||||
#else
|
||||
"%s/oakengine_disk_test_default_XXXXXX",
|
||||
#endif
|
||||
getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp");
|
||||
|
||||
#if defined(_WIN32)
|
||||
char *tmpdir = _mktemp(tmp);
|
||||
assert(tmpdir != NULL);
|
||||
assert(_mkdir(tmpdir) == 0);
|
||||
#else
|
||||
char *tmpdir = mkdtemp(tmp);
|
||||
assert(tmpdir != NULL);
|
||||
#endif
|
||||
|
||||
assert(oakengine_disk_set_default_cache_path(tmpdir) == OAKENGINE_OK);
|
||||
|
||||
char updated[512];
|
||||
assert(oakengine_disk_get_default_cache_path(updated, sizeof(updated)) > 0);
|
||||
assert(strcmp(updated, tmpdir) == 0);
|
||||
|
||||
// Restore original default path.
|
||||
assert(oakengine_disk_set_default_cache_path(original) == OAKENGINE_OK);
|
||||
assert(oakengine_disk_get_default_cache_path(updated, sizeof(updated)) > 0);
|
||||
assert(strcmp(updated, original) == 0);
|
||||
|
||||
#if defined(_WIN32)
|
||||
_rmdir(tmpdir);
|
||||
#else
|
||||
rmdir(tmpdir);
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_instance_lifecycle();
|
||||
test_default_cache_path();
|
||||
test_open_folder_handle();
|
||||
test_clear_cache();
|
||||
test_settings_handler_round_trip();
|
||||
test_set_default_cache_path();
|
||||
test_invalidate_project();
|
||||
|
||||
// Leave DiskManager in the initialized state for shutdown.
|
||||
oakengine_disk_create_instance();
|
||||
|
||||
oakengine_shutdown();
|
||||
|
||||
printf("oakengine_disk_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
/***
|
||||
|
||||
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 encoding facade
|
||||
// (oakengine/encoding.h + oakengine/videoparams.h). Exercises the
|
||||
// format/codec metadata queries, the image-sequence filename helpers, the
|
||||
// scaling matrix, the OakEngineEncodingParams handle (getter/setter
|
||||
// roundtrips, preset file load/save) and the VideoParams static data behind
|
||||
// the standard combo boxes. No GPU: the export execution path itself is
|
||||
// covered by oakengine_export_test; here only the error paths of
|
||||
// render_with_params are touched (no sequence).
|
||||
|
||||
#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/encoding.h"
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/videoparams.h"
|
||||
|
||||
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_encoding_test_%lu",
|
||||
base, (unsigned long)GetCurrentProcessId());
|
||||
assert(_mkdir(g_tmpdir) == 0);
|
||||
#else
|
||||
strcpy(g_tmpdir, "/tmp/oakengine_encoding_test_XXXXXX");
|
||||
assert(mkdtemp(g_tmpdir) != NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void test_format_metadata(void)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
assert(oakengine_encoding_format_count() > 0);
|
||||
|
||||
// Matroska
|
||||
assert(oakengine_encoding_format_name(OAKENGINE_ENCODING_FORMAT_MATROSKA,
|
||||
buf, sizeof(buf)) > 0);
|
||||
assert(strstr(buf, "Matroska") != NULL);
|
||||
assert(oakengine_encoding_format_extension(
|
||||
OAKENGINE_ENCODING_FORMAT_MATROSKA, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "mkv") == 0);
|
||||
|
||||
// Invalid format
|
||||
assert(oakengine_encoding_format_name(-1, buf, sizeof(buf)) == -1);
|
||||
assert(oakengine_encoding_format_extension(9999, buf, sizeof(buf)) == -1);
|
||||
assert(oakengine_encoding_format_video_codec_count(-1) == -1);
|
||||
|
||||
// MP4 carries H.264 video and AAC audio
|
||||
const int vcount =
|
||||
oakengine_encoding_format_video_codec_count(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO);
|
||||
assert(vcount > 0);
|
||||
int found_h264 = 0;
|
||||
for (int i = 0; i < vcount; i++) {
|
||||
if (oakengine_encoding_format_video_codec_at(
|
||||
OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO, i) ==
|
||||
OAKENGINE_ENCODING_CODEC_H264) {
|
||||
found_h264 = 1;
|
||||
}
|
||||
}
|
||||
assert(found_h264);
|
||||
assert(oakengine_encoding_format_video_codec_at(
|
||||
OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO, vcount) == -1);
|
||||
|
||||
// WAV is audio-only and carries PCM
|
||||
assert(oakengine_encoding_format_video_codec_count(
|
||||
OAKENGINE_ENCODING_FORMAT_WAV) == 0);
|
||||
const int acount = oakengine_encoding_format_audio_codec_count(
|
||||
OAKENGINE_ENCODING_FORMAT_WAV);
|
||||
assert(acount > 0);
|
||||
int found_pcm = 0;
|
||||
for (int i = 0; i < acount; i++) {
|
||||
if (oakengine_encoding_format_audio_codec_at(
|
||||
OAKENGINE_ENCODING_FORMAT_WAV, i) == OAKENGINE_ENCODING_CODEC_PCM) {
|
||||
found_pcm = 1;
|
||||
}
|
||||
}
|
||||
assert(found_pcm);
|
||||
|
||||
// SRT is subtitles-only
|
||||
assert(oakengine_encoding_format_subtitle_codec_count(
|
||||
OAKENGINE_ENCODING_FORMAT_SRT) > 0);
|
||||
assert(oakengine_encoding_format_subtitle_codec_at(
|
||||
OAKENGINE_ENCODING_FORMAT_SRT, 0) >= 0);
|
||||
assert(oakengine_encoding_format_subtitle_codec_at(
|
||||
OAKENGINE_ENCODING_FORMAT_SRT, -1) < 0);
|
||||
assert(oakengine_encoding_format_audio_codec_count(
|
||||
OAKENGINE_ENCODING_FORMAT_SRT) == 0);
|
||||
}
|
||||
|
||||
static void test_codec_metadata(void)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
assert(oakengine_encoding_codec_name(OAKENGINE_ENCODING_CODEC_H264, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(buf[0] != '\0');
|
||||
assert(oakengine_encoding_codec_name(-1, buf, sizeof(buf)) == -1);
|
||||
|
||||
assert(oakengine_encoding_codec_is_still_image(5 /* PNG */) == 1);
|
||||
assert(oakengine_encoding_codec_is_still_image(
|
||||
OAKENGINE_ENCODING_CODEC_H264) == 0);
|
||||
assert(oakengine_encoding_codec_is_lossless(OAKENGINE_ENCODING_CODEC_PCM) ==
|
||||
1);
|
||||
assert(oakengine_encoding_codec_is_lossless(OAKENGINE_ENCODING_CODEC_AAC) ==
|
||||
0);
|
||||
|
||||
// Encoded pixel formats of H.264 in MP4: yuv420p is the preferred one
|
||||
const int pcount = oakengine_encoding_pix_fmt_count(
|
||||
OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO, OAKENGINE_ENCODING_CODEC_H264);
|
||||
assert(pcount > 0);
|
||||
assert(oakengine_encoding_pix_fmt_at(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO,
|
||||
OAKENGINE_ENCODING_CODEC_H264, 0, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "yuv420p") == 0);
|
||||
assert(oakengine_encoding_pix_fmt_at(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO,
|
||||
OAKENGINE_ENCODING_CODEC_H264, pcount,
|
||||
buf, sizeof(buf)) == -1);
|
||||
assert(oakengine_encoding_pix_fmt_index(OAKENGINE_ENCODING_CODEC_H264,
|
||||
"yuv420p") == 0);
|
||||
assert(oakengine_encoding_pix_fmt_index(OAKENGINE_ENCODING_CODEC_H264,
|
||||
"no-such-format") == 0);
|
||||
assert(oakengine_encoding_pix_fmt_index(OAKENGINE_ENCODING_CODEC_H264,
|
||||
NULL) == 0);
|
||||
|
||||
// Sample formats of PCM in WAV
|
||||
const int scount = oakengine_encoding_sample_format_count(
|
||||
OAKENGINE_ENCODING_FORMAT_WAV, OAKENGINE_ENCODING_CODEC_PCM);
|
||||
assert(scount > 0);
|
||||
for (int i = 0; i < scount; i++) {
|
||||
assert(oakengine_encoding_sample_format_at(
|
||||
OAKENGINE_ENCODING_FORMAT_WAV, OAKENGINE_ENCODING_CODEC_PCM,
|
||||
i) >= 0);
|
||||
}
|
||||
assert(oakengine_encoding_sample_format_at(
|
||||
OAKENGINE_ENCODING_FORMAT_WAV, OAKENGINE_ENCODING_CODEC_PCM,
|
||||
scount) == -1);
|
||||
}
|
||||
|
||||
static void test_filename_helpers(void)
|
||||
{
|
||||
char buf[4096];
|
||||
|
||||
assert(oakengine_encoding_filename_contains_digit_placeholder(
|
||||
"/tmp/out_[#####].png") == 1);
|
||||
assert(oakengine_encoding_filename_contains_digit_placeholder(
|
||||
"/tmp/out.png") == 0);
|
||||
assert(oakengine_encoding_filename_contains_digit_placeholder(NULL) == 0);
|
||||
|
||||
assert(oakengine_encoding_image_sequence_digit_count(
|
||||
"/tmp/out_[#####].png") == 5);
|
||||
assert(oakengine_encoding_image_sequence_digit_count("/tmp/out.png") == 0);
|
||||
|
||||
assert(oakengine_encoding_filename_remove_digit_placeholder(
|
||||
"/tmp/out_[#####].png", buf, sizeof(buf)) > 0);
|
||||
assert(strstr(buf, "[#####]") == NULL);
|
||||
assert(strstr(buf, ".png") != NULL);
|
||||
}
|
||||
|
||||
static void test_generate_matrix(void)
|
||||
{
|
||||
float m[16];
|
||||
|
||||
// Fit with matching dimensions is the identity
|
||||
assert(oakengine_encoding_generate_matrix(OAKENGINE_ENCODING_SCALING_FIT,
|
||||
1920, 1080, 1920, 1080,
|
||||
m) == OAKENGINE_OK);
|
||||
const float identity[16] = { 1, 0, 0, 0, 0, 1, 0, 0,
|
||||
0, 0, 1, 0, 0, 0, 0, 1 };
|
||||
for (int i = 0; i < 16; i++) {
|
||||
assert(fabsf(m[i] - identity[i]) < 1e-6f);
|
||||
}
|
||||
|
||||
// Stretch is the identity transform (the preview is normalized device
|
||||
// coordinates; stretching needs no matrix)
|
||||
assert(oakengine_encoding_generate_matrix(OAKENGINE_ENCODING_SCALING_STRETCH,
|
||||
960, 540, 1920, 1080,
|
||||
m) == OAKENGINE_OK);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
assert(fabsf(m[i] - identity[i]) < 1e-6f);
|
||||
}
|
||||
|
||||
// Fit into a wider-than-source frame pillarboxes: x scale shrinks to
|
||||
// source_ar/export_ar
|
||||
assert(oakengine_encoding_generate_matrix(OAKENGINE_ENCODING_SCALING_FIT,
|
||||
1920, 1080, 1920, 540,
|
||||
m) == OAKENGINE_OK);
|
||||
const float expected_x = (1920.0f / 1080.0f) / (1920.0f / 540.0f);
|
||||
assert(fabsf(m[0] - expected_x) < 1e-5f);
|
||||
assert(fabsf(m[5] - 1.0f) < 1e-6f);
|
||||
|
||||
// Invalid arguments
|
||||
assert(oakengine_encoding_generate_matrix(-1, 1, 1, 1, 1,
|
||||
m) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_encoding_generate_matrix(OAKENGINE_ENCODING_SCALING_FIT, 0,
|
||||
1, 1, 1,
|
||||
m) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_params_handle(void)
|
||||
{
|
||||
char buf[1024];
|
||||
|
||||
OakEngineEncodingParams *p = oakengine_encoding_params_create();
|
||||
assert(p != NULL);
|
||||
|
||||
// Fresh handle: nothing enabled, format unset
|
||||
assert(oakengine_encoding_params_is_valid(p) == 0);
|
||||
assert(oakengine_encoding_params_format(p) == -1);
|
||||
assert(oakengine_encoding_params_video_enabled(p) == 0);
|
||||
assert(oakengine_encoding_params_audio_enabled(p) == 0);
|
||||
assert(oakengine_encoding_params_subtitles_enabled(p) == 0);
|
||||
assert(oakengine_encoding_params_has_custom_range(p) == 0);
|
||||
|
||||
// Filename / format roundtrip
|
||||
assert(oakengine_encoding_params_set_filename(p, "/tmp/out.mp4") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_filename(p, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "/tmp/out.mp4") == 0);
|
||||
assert(oakengine_encoding_params_set_format(
|
||||
p, OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO) == OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_format(p) ==
|
||||
OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO);
|
||||
assert(oakengine_encoding_params_set_format(p, 9999) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Video roundtrip
|
||||
oak_video_params v = {};
|
||||
v.width = 1920;
|
||||
v.height = 1080;
|
||||
v.time_base_num = 1001;
|
||||
v.time_base_den = 30000;
|
||||
v.format = 8; /* a PixelFormat::Format value */
|
||||
v.pixel_aspect_num = 1;
|
||||
v.pixel_aspect_den = 1;
|
||||
v.interlacing = OAKENGINE_ENCODING_INTERLACE_BOTTOM_FIRST;
|
||||
v.color_range = OAKENGINE_ENCODING_COLOR_RANGE_FULL;
|
||||
v.divider = 1;
|
||||
assert(oakengine_encoding_params_enable_video(
|
||||
p, &v, OAKENGINE_ENCODING_CODEC_H264) == OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_is_valid(p) == 1);
|
||||
assert(oakengine_encoding_params_video_enabled(p) == 1);
|
||||
assert(oakengine_encoding_params_video_codec(p) ==
|
||||
OAKENGINE_ENCODING_CODEC_H264);
|
||||
assert(oakengine_encoding_params_enable_video(p, NULL, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_encoding_params_enable_video(p, &v, 9999) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
oak_video_params back = {};
|
||||
assert(oakengine_encoding_params_get_video_params(p, &back) ==
|
||||
OAKENGINE_OK);
|
||||
assert(back.width == 1920 && back.height == 1080);
|
||||
assert(back.time_base_num == 1001 && back.time_base_den == 30000);
|
||||
assert(back.pixel_aspect_num == 1 && back.pixel_aspect_den == 1);
|
||||
assert(back.interlacing == OAKENGINE_ENCODING_INTERLACE_BOTTOM_FIRST);
|
||||
assert(back.color_range == OAKENGINE_ENCODING_COLOR_RANGE_FULL);
|
||||
|
||||
// Audio roundtrip
|
||||
assert(oakengine_encoding_params_enable_audio(p, 48000, 0x3, 4,
|
||||
OAKENGINE_ENCODING_CODEC_AAC) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_audio_enabled(p) == 1);
|
||||
assert(oakengine_encoding_params_audio_codec(p) ==
|
||||
OAKENGINE_ENCODING_CODEC_AAC);
|
||||
int sample_rate = 0, sample_format = 0;
|
||||
uint64_t layout = 0;
|
||||
assert(oakengine_encoding_params_get_audio_params(p, &sample_rate, &layout,
|
||||
&sample_format) ==
|
||||
OAKENGINE_OK);
|
||||
assert(sample_rate == 48000 && layout == 0x3 && sample_format == 4);
|
||||
assert(oakengine_encoding_params_enable_audio(p, 0, 0x3, 4, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Subtitles (embedded, then sidecar)
|
||||
assert(oakengine_encoding_params_enable_subtitles(
|
||||
p, OAKENGINE_ENCODING_CODEC_SRT) == OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_subtitles_enabled(p) == 1);
|
||||
assert(oakengine_encoding_params_subtitles_are_sidecar(p) == 0);
|
||||
assert(oakengine_encoding_params_subtitles_codec(p) ==
|
||||
OAKENGINE_ENCODING_CODEC_SRT);
|
||||
assert(oakengine_encoding_params_enable_sidecar_subtitles(
|
||||
p, OAKENGINE_ENCODING_FORMAT_SRT,
|
||||
OAKENGINE_ENCODING_CODEC_SRT) == OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_subtitles_are_sidecar(p) == 1);
|
||||
assert(oakengine_encoding_params_subtitles_sidecar_format(p) ==
|
||||
OAKENGINE_ENCODING_FORMAT_SRT);
|
||||
|
||||
// Scalar setters/getters
|
||||
oakengine_encoding_params_set_video_bit_rate(p, 8000000);
|
||||
assert(oakengine_encoding_params_video_bit_rate(p) == 8000000);
|
||||
oakengine_encoding_params_set_video_min_bit_rate(p, 1000);
|
||||
assert(oakengine_encoding_params_video_min_bit_rate(p) == 1000);
|
||||
oakengine_encoding_params_set_video_max_bit_rate(p, 16000000);
|
||||
assert(oakengine_encoding_params_video_max_bit_rate(p) == 16000000);
|
||||
oakengine_encoding_params_set_video_buffer_size(p, 2000000);
|
||||
assert(oakengine_encoding_params_video_buffer_size(p) == 2000000);
|
||||
oakengine_encoding_params_set_video_threads(p, 4);
|
||||
assert(oakengine_encoding_params_video_threads(p) == 4);
|
||||
oakengine_encoding_params_set_audio_bit_rate(p, 320000);
|
||||
assert(oakengine_encoding_params_audio_bit_rate(p) == 320000);
|
||||
|
||||
assert(oakengine_encoding_params_set_video_pix_fmt(p, "yuv420p") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_video_pix_fmt(p, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "yuv420p") == 0);
|
||||
|
||||
oakengine_encoding_params_set_video_is_image_sequence(p, 1);
|
||||
assert(oakengine_encoding_params_video_is_image_sequence(p) == 1);
|
||||
oakengine_encoding_params_set_video_is_image_sequence(p, 0);
|
||||
assert(oakengine_encoding_params_video_is_image_sequence(p) == 0);
|
||||
|
||||
assert(oakengine_encoding_params_set_color_transform(p, "sRGB OETF") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_color_transform_output(p, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "sRGB OETF") == 0);
|
||||
|
||||
oakengine_encoding_params_set_export_length(p, 10, 1);
|
||||
int num = 0, den = 0;
|
||||
assert(oakengine_encoding_params_get_export_length(p, &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 10 && den == 1);
|
||||
|
||||
// Custom range
|
||||
oakengine_encoding_params_set_custom_range(p, 1, 1, 5, 1);
|
||||
assert(oakengine_encoding_params_has_custom_range(p) == 1);
|
||||
int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0;
|
||||
assert(oakengine_encoding_params_get_custom_range(p, &in_num, &in_den,
|
||||
&out_num,
|
||||
&out_den) == OAKENGINE_OK);
|
||||
assert(in_num == 1 && in_den == 1 && out_num == 5 && out_den == 1);
|
||||
|
||||
// Scaling method
|
||||
assert(oakengine_encoding_params_set_video_scaling_method(
|
||||
p, OAKENGINE_ENCODING_SCALING_CROP) == OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_video_scaling_method(p) ==
|
||||
OAKENGINE_ENCODING_SCALING_CROP);
|
||||
assert(oakengine_encoding_params_set_video_scaling_method(p, 42) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Video options
|
||||
assert(oakengine_encoding_params_video_option(p, "crf", buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_encoding_params_set_video_option(p, "crf", "18") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_video_option(p, "crf", buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "18") == 0);
|
||||
|
||||
// Disables
|
||||
oakengine_encoding_params_disable_subtitles(p);
|
||||
assert(oakengine_encoding_params_subtitles_enabled(p) == 0);
|
||||
oakengine_encoding_params_disable_video(p);
|
||||
assert(oakengine_encoding_params_video_enabled(p) == 0);
|
||||
assert(oakengine_encoding_params_get_video_params(p, &back) ==
|
||||
OAKENGINE_E_STATE);
|
||||
oakengine_encoding_params_disable_audio(p);
|
||||
assert(oakengine_encoding_params_audio_enabled(p) == 0);
|
||||
|
||||
// NULL safety
|
||||
oakengine_encoding_params_destroy(NULL);
|
||||
assert(oakengine_encoding_params_is_valid(NULL) == 0);
|
||||
|
||||
oakengine_encoding_params_destroy(p);
|
||||
}
|
||||
|
||||
static void test_preset_files(void)
|
||||
{
|
||||
char buf[1024];
|
||||
|
||||
// Preset directory listing is readable (may be empty in the sandbox)
|
||||
assert(oakengine_encoding_preset_path(buf, sizeof(buf)) > 0);
|
||||
const int count = oakengine_encoding_preset_count();
|
||||
assert(count >= 0);
|
||||
for (int i = 0; i < count; i++) {
|
||||
assert(oakengine_encoding_preset_name(i, buf, sizeof(buf)) > 0);
|
||||
}
|
||||
assert(oakengine_encoding_preset_name(count, buf, sizeof(buf)) == -1);
|
||||
|
||||
// Save/load roundtrip through a temp file
|
||||
char path[4096];
|
||||
snprintf(path, sizeof(path), "%s/preset.xml", g_tmpdir);
|
||||
|
||||
OakEngineEncodingParams *p = oakengine_encoding_params_create();
|
||||
assert(oakengine_encoding_params_set_filename(p, "/tmp/out.mp4") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_set_format(
|
||||
p, OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO) == OAKENGINE_OK);
|
||||
oak_video_params v = {};
|
||||
v.width = 1280;
|
||||
v.height = 720;
|
||||
v.time_base_num = 1;
|
||||
v.time_base_den = 25;
|
||||
v.format = 8;
|
||||
v.pixel_aspect_num = 1;
|
||||
v.pixel_aspect_den = 1;
|
||||
v.interlacing = OAKENGINE_ENCODING_INTERLACE_NONE;
|
||||
v.color_range = OAKENGINE_ENCODING_COLOR_RANGE_LIMITED;
|
||||
v.divider = 1;
|
||||
assert(oakengine_encoding_params_enable_video(
|
||||
p, &v, OAKENGINE_ENCODING_CODEC_H264) == OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_set_video_option(p, "crf", "20") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_save_file(p, path) == OAKENGINE_OK);
|
||||
oakengine_encoding_params_destroy(p);
|
||||
|
||||
OakEngineEncodingParams *q = oakengine_encoding_params_create();
|
||||
assert(oakengine_encoding_params_load_file(q, path) == OAKENGINE_OK);
|
||||
assert(oakengine_encoding_params_video_enabled(q) == 1);
|
||||
assert(oakengine_encoding_params_video_codec(q) ==
|
||||
OAKENGINE_ENCODING_CODEC_H264);
|
||||
oak_video_params back = {};
|
||||
assert(oakengine_encoding_params_get_video_params(q, &back) ==
|
||||
OAKENGINE_OK);
|
||||
assert(back.width == 1280 && back.height == 720);
|
||||
assert(back.time_base_num == 1 && back.time_base_den == 25);
|
||||
assert(oakengine_encoding_params_video_option(q, "crf", buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "20") == 0);
|
||||
oakengine_encoding_params_destroy(q);
|
||||
|
||||
// Loading a nonexistent file fails
|
||||
OakEngineEncodingParams *r = oakengine_encoding_params_create();
|
||||
assert(oakengine_encoding_params_load_file(r, "/no/such/file.xml") ==
|
||||
OAKENGINE_E_FAILED);
|
||||
oakengine_encoding_params_destroy(r);
|
||||
|
||||
// Bad arguments
|
||||
assert(oakengine_encoding_params_save_file(NULL, path) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_last_used_and_render_errors(void)
|
||||
{
|
||||
// NULL sequence: no last-used params, no-op setter
|
||||
assert(oakengine_encoding_params_get_last_used(NULL) == NULL);
|
||||
oakengine_encoding_params_set_last_used(NULL, NULL);
|
||||
|
||||
// render_with_params without a valid sequence/params fails cleanly
|
||||
assert(oakengine_export_render_with_params(NULL, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
OakEngineEncodingParams *p = oakengine_encoding_params_create();
|
||||
assert(oakengine_export_render_with_params(NULL, p) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
// Nothing enabled on the handle
|
||||
assert(oakengine_export_render_with_params(
|
||||
reinterpret_cast<OakEngineSequence *>(p), p) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
oakengine_encoding_params_destroy(p);
|
||||
|
||||
// Audio recording requires an enabled audio track on the handle
|
||||
assert(oakengine_encoding_start_audio_recording(NULL, NULL, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_video_params_statics(void)
|
||||
{
|
||||
char buf[256];
|
||||
int num = 0, den = 0;
|
||||
|
||||
// Standard frame rates
|
||||
const int fr_count = oakengine_video_params_supported_frame_rate_count();
|
||||
assert(fr_count > 0);
|
||||
for (int i = 0; i < fr_count; i++) {
|
||||
assert(oakengine_video_params_supported_frame_rate_at(i, &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num > 0 && den > 0);
|
||||
}
|
||||
assert(oakengine_video_params_supported_frame_rate_at(fr_count, &num,
|
||||
&den) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// 24000/1001 prints as 23.976...
|
||||
assert(oakengine_video_params_frame_rate_to_string(24000, 1001, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strstr(buf, "23.97") != NULL);
|
||||
|
||||
// Standard pixel aspects: the first one is square (1:1)
|
||||
const int pa_count = oakengine_video_params_standard_pixel_aspect_count();
|
||||
assert(pa_count > 0);
|
||||
assert(oakengine_video_params_standard_pixel_aspect_at(0, &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 1 && den == 1);
|
||||
assert(oakengine_video_params_standard_pixel_aspect_name(0, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(buf[0] != '\0');
|
||||
assert(oakengine_video_params_standard_pixel_aspect_at(pa_count, &num,
|
||||
&den) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Custom PAR label template
|
||||
assert(oakengine_video_params_format_pixel_aspect_ratio_string(
|
||||
"Custom (%1)", 32, 27, buf, sizeof(buf)) > 0);
|
||||
assert(strstr(buf, "Custom") != NULL);
|
||||
|
||||
// Dividers
|
||||
const int div_count = oakengine_video_params_supported_divider_count();
|
||||
assert(div_count > 0);
|
||||
for (int i = 0; i < div_count; i++) {
|
||||
const int d = oakengine_video_params_supported_divider_at(i);
|
||||
assert(d > 0);
|
||||
assert(oakengine_video_params_divider_name(d, buf, sizeof(buf)) > 0);
|
||||
}
|
||||
assert(oakengine_video_params_supported_divider_at(div_count) == -1);
|
||||
|
||||
// Pixel format names: some entry must be non-empty
|
||||
assert(oakengine_video_params_pixel_format_name(8, buf, sizeof(buf)) > 0);
|
||||
|
||||
// Float detection (8-bit integer formats are not float)
|
||||
assert(oakengine_video_params_format_is_float(0) == 0);
|
||||
|
||||
// Effective (divider-scaled) size
|
||||
int w = 0, h = 0;
|
||||
assert(oakengine_video_params_effective_size(1920, 1080, 2, &w, &h) ==
|
||||
OAKENGINE_OK);
|
||||
assert(w == 960 && h == 540);
|
||||
assert(oakengine_video_params_effective_size(0, 1080, 2, &w, &h) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
// POD mirror of the display-path VideoParams (B7): make/equal/is_valid,
|
||||
// bytes-per-pixel and the internal channel count.
|
||||
static void test_video_params_pod(void)
|
||||
{
|
||||
oak_video_params p, q;
|
||||
|
||||
// make fills every field
|
||||
assert(oakengine_video_params_make(&p, 1920, 1080, 1001, 30000, 0, 1, 1,
|
||||
0, 0, 2) == OAKENGINE_OK);
|
||||
assert(p.width == 1920 && p.height == 1080);
|
||||
assert(p.time_base_num == 1001 && p.time_base_den == 30000);
|
||||
assert(p.pixel_aspect_num == 1 && p.pixel_aspect_den == 1);
|
||||
assert(p.divider == 2);
|
||||
assert(oakengine_video_params_make(NULL, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// equal: identical PODs match, any single-field difference does not
|
||||
q = p;
|
||||
assert(oakengine_video_params_equal(&p, &q) == 1);
|
||||
assert(oakengine_video_params_equal(&p, NULL) == 0);
|
||||
assert(oakengine_video_params_equal(NULL, &q) == 0);
|
||||
q.divider = 1;
|
||||
assert(oakengine_video_params_equal(&p, &q) == 0);
|
||||
q = p;
|
||||
q.interlacing = 1;
|
||||
assert(oakengine_video_params_equal(&p, &q) == 0);
|
||||
|
||||
// is_valid: positive dimensions + in-range format passes; zero
|
||||
// dimensions or an out-of-range format fail
|
||||
assert(oakengine_video_params_is_valid(&p) == 1);
|
||||
assert(oakengine_video_params_is_valid(NULL) == 0);
|
||||
q = p;
|
||||
q.width = 0;
|
||||
assert(oakengine_video_params_is_valid(&q) == 0);
|
||||
q = p;
|
||||
q.format = -1; // olive::PixelFormat::invalid
|
||||
assert(oakengine_video_params_is_valid(&q) == 0);
|
||||
|
||||
// bytes per pixel: u8 RGBA = 4, f32 RGBA = 16 (format values follow
|
||||
// olive::PixelFormat::Format: 0 = u8, 4 = f32)
|
||||
const int channels = oakengine_video_params_internal_channel_count();
|
||||
assert(channels == 4);
|
||||
assert(oakengine_video_params_bytes_per_pixel(0, channels) == 4);
|
||||
assert(oakengine_video_params_bytes_per_pixel(4, channels) == 16);
|
||||
}
|
||||
|
||||
// Engine-side VideoParams construction used by the app during R6 to avoid
|
||||
// pulling C++ constructors into oak-editor.
|
||||
static void test_video_params_create_free(void)
|
||||
{
|
||||
// NULL pod -> NULL handle
|
||||
assert(oakengine_video_params_create(NULL) == NULL);
|
||||
|
||||
// Empty POD -> default-constructed VideoParams handle
|
||||
oak_video_params empty = {};
|
||||
void *vp_empty = oakengine_video_params_create(&empty);
|
||||
assert(vp_empty != NULL);
|
||||
oakengine_video_params_free(vp_empty);
|
||||
|
||||
// Display-path POD with explicit timebase
|
||||
oak_video_params pod;
|
||||
assert(oakengine_video_params_make(&pod, 1920, 1080, 1001, 30000, 0, 1, 1,
|
||||
0, 0, 1) == OAKENGINE_OK);
|
||||
void *vp = oakengine_video_params_create(&pod);
|
||||
assert(vp != NULL);
|
||||
oakengine_video_params_free(vp);
|
||||
|
||||
// Display-path POD without timebase (uses constructor without timebase)
|
||||
oak_video_params pod2 = {};
|
||||
pod2.width = 640;
|
||||
pod2.height = 480;
|
||||
pod2.format = 0; // u8
|
||||
void *vp2 = oakengine_video_params_create(&pod2);
|
||||
assert(vp2 != NULL);
|
||||
oakengine_video_params_free(vp2);
|
||||
|
||||
// free(NULL) is a no-op
|
||||
oakengine_video_params_free(NULL);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
|
||||
// HEADLESS: no GL, but a QCoreApplication (needed by the FFmpeg encoder
|
||||
// probes and QStandardPaths behind the metadata queries) comes up.
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_format_metadata();
|
||||
test_codec_metadata();
|
||||
test_filename_helpers();
|
||||
test_generate_matrix();
|
||||
test_params_handle();
|
||||
test_preset_files();
|
||||
test_last_used_and_render_errors();
|
||||
test_video_params_statics();
|
||||
test_video_params_create_free();
|
||||
|
||||
oakengine_shutdown();
|
||||
|
||||
printf("oakengine_encoding_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,960 @@
|
||||
/***
|
||||
|
||||
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 event subscription family
|
||||
// (oakengine/events.h) and the track block traversal family
|
||||
// (oakengine_track_nearest_block_* / oakengine_block_*). Every subscription
|
||||
// is exercised by provoking a real engine change through the facade and
|
||||
// asserting the callback fired with the documented payload. No GL required.
|
||||
|
||||
#include <assert.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/events.h"
|
||||
#include "oakengine/footage.h"
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/node.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/timeline.h"
|
||||
#include "oakengine/viewer.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_events_test_%lu", base,
|
||||
(unsigned long)GetCurrentProcessId());
|
||||
assert(_mkdir(g_tmpdir) == 0);
|
||||
#else
|
||||
strcpy(g_tmpdir, "/tmp/oakengine_events_test_XXXXXX");
|
||||
assert(mkdtemp(g_tmpdir) != NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void demo_path(char *dst, size_t cap)
|
||||
{
|
||||
const int n = snprintf(dst, cap, "%s/tests/demo.mp4", OAK_TEST_SOURCE_DIR);
|
||||
assert(n > 0 && (size_t)n < cap);
|
||||
}
|
||||
|
||||
// Callback recorder: counts deliveries per event id and keeps the last
|
||||
// payload of each.
|
||||
#define MAX_TRACKED_EVENT 128
|
||||
|
||||
typedef struct {
|
||||
int count[MAX_TRACKED_EVENT];
|
||||
int64_t last_a[MAX_TRACKED_EVENT];
|
||||
int64_t last_b[MAX_TRACKED_EVENT];
|
||||
int64_t last_c[MAX_TRACKED_EVENT];
|
||||
void *last_source[MAX_TRACKED_EVENT];
|
||||
void *last_handle[MAX_TRACKED_EVENT];
|
||||
char last_s[MAX_TRACKED_EVENT][256];
|
||||
} EventLog;
|
||||
|
||||
static void record_event(const oakengine_event *event, void *userdata)
|
||||
{
|
||||
EventLog *log = (EventLog *)userdata;
|
||||
assert(event != NULL);
|
||||
assert(event->id > 0 && event->id < MAX_TRACKED_EVENT);
|
||||
log->count[event->id]++;
|
||||
log->last_a[event->id] = event->a;
|
||||
log->last_b[event->id] = event->b;
|
||||
log->last_c[event->id] = event->c;
|
||||
log->last_source[event->id] = event->source;
|
||||
log->last_handle[event->id] = event->handle;
|
||||
snprintf(log->last_s[event->id], sizeof(log->last_s[event->id]), "%s",
|
||||
event->s ? event->s : "");
|
||||
}
|
||||
|
||||
static void reset_event(EventLog *log, int id)
|
||||
{
|
||||
log->count[id] = 0;
|
||||
}
|
||||
|
||||
// ---- Subscription validation ----------------------------------------------
|
||||
|
||||
static void test_subscribe_validation(OakEngineProject *project,
|
||||
OakEngineSequence *seq,
|
||||
OakEngineTrack *track)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
// NULL handle / NULL callback / unknown event id.
|
||||
assert(oakengine_event_subscribe(
|
||||
NULL, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED, record_event,
|
||||
&log) == 0);
|
||||
assert(oakengine_event_subscribe(project,
|
||||
OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED,
|
||||
NULL, &log) == 0);
|
||||
assert(oakengine_event_subscribe(project, 999, record_event, &log) == 0);
|
||||
|
||||
// Handle/event family mismatches.
|
||||
assert(oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED, record_event,
|
||||
&log) == 0);
|
||||
assert(oakengine_event_subscribe(project,
|
||||
OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED,
|
||||
record_event, &log) == 0);
|
||||
assert(oakengine_event_subscribe(track,
|
||||
OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM,
|
||||
record_event, &log) == 0);
|
||||
assert(oakengine_event_subscribe(project,
|
||||
OAKENGINE_EVENT_TRACK_BLOCK_ADDED,
|
||||
record_event, &log) == 0);
|
||||
|
||||
// Bad unsubscribe arguments.
|
||||
assert(oakengine_event_unsubscribe(0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_event_unsubscribe(-5) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_event_unsubscribe(424242) == OAKENGINE_E_NOT_FOUND);
|
||||
}
|
||||
|
||||
// ---- Project events ---------------------------------------------------------
|
||||
|
||||
static void test_project_events(OakEngineProject *project)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
// Normalize to unmodified first: modified_changed only fires on an
|
||||
// actual flip, and the setup above already dirtied the project.
|
||||
oakengine_project_set_modified(project, 0);
|
||||
|
||||
int64_t sub = oakengine_event_subscribe(
|
||||
project, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED, record_event, &log);
|
||||
assert(sub > 0);
|
||||
|
||||
oakengine_project_set_modified(project, 1);
|
||||
assert(log.count[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 1);
|
||||
assert(log.last_source[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] ==
|
||||
(void *)project);
|
||||
|
||||
oakengine_project_set_modified(project, 0);
|
||||
assert(log.count[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 2);
|
||||
assert(log.last_a[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 0);
|
||||
|
||||
// After unsubscribing no further events arrive.
|
||||
assert(oakengine_event_unsubscribe(sub) == OAKENGINE_OK);
|
||||
oakengine_project_set_modified(project, 1);
|
||||
assert(log.count[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 2);
|
||||
|
||||
// Unsubscribing twice is a not-found no-op.
|
||||
assert(oakengine_event_unsubscribe(sub) == OAKENGINE_E_NOT_FOUND);
|
||||
}
|
||||
|
||||
// ---- Folder events ----------------------------------------------------------
|
||||
|
||||
static void test_folder_events(OakEngineProject *project)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
// A fresh project's first node is its root folder (same fixture as
|
||||
// oakengine_footage_test).
|
||||
OakEngineNode *root = oakengine_project_node_at(project, 0);
|
||||
assert(root != NULL);
|
||||
|
||||
int64_t sub_begin = oakengine_event_subscribe(
|
||||
root, OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM, record_event, &log);
|
||||
int64_t sub_end = oakengine_event_subscribe(
|
||||
root, OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM, record_event, &log);
|
||||
int64_t sub_rm_begin = oakengine_event_subscribe(
|
||||
root, OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM, record_event, &log);
|
||||
int64_t sub_rm_end = oakengine_event_subscribe(
|
||||
root, OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM, record_event, &log);
|
||||
assert(sub_begin > 0 && sub_end > 0 && sub_rm_begin > 0 &&
|
||||
sub_rm_end > 0);
|
||||
|
||||
OakEngineNode *folder = oakengine_folder_create(project, root, "Sub");
|
||||
assert(folder != NULL);
|
||||
|
||||
assert(log.count[OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM] == 1);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM] ==
|
||||
(void *)folder);
|
||||
assert(log.last_a[OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM] >= 0);
|
||||
assert(log.count[OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM] == 1);
|
||||
|
||||
// Undoing the folder creation removes it from the root again.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM] == 1);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM] ==
|
||||
(void *)folder);
|
||||
assert(log.count[OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM] == 1);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
|
||||
assert(oakengine_event_unsubscribe(sub_begin) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_end) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_rm_begin) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_rm_end) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Sequence / track events -------------------------------------------------
|
||||
|
||||
static void test_sequence_events(OakEngineProject *project,
|
||||
OakEngineSequence *seq,
|
||||
const char *media_path)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
// Track added: subscribe, then append an audio track.
|
||||
int64_t sub_track = oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED, record_event, &log);
|
||||
assert(sub_track > 0);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
|
||||
0);
|
||||
assert(log.count[OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED] ==
|
||||
OAKENGINE_TRACK_TYPE_AUDIO);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED] != NULL);
|
||||
assert(log.last_source[OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED] ==
|
||||
(void *)seq);
|
||||
assert(oakengine_event_unsubscribe(sub_track) == OAKENGINE_OK);
|
||||
|
||||
// Block added on the video track (track 0 was created by the caller).
|
||||
OakEngineTrack *track = oakengine_sequence_track_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0);
|
||||
assert(track != NULL);
|
||||
int64_t sub_block = oakengine_event_subscribe(
|
||||
track, OAKENGINE_EVENT_TRACK_BLOCK_ADDED, record_event, &log);
|
||||
assert(sub_block > 0);
|
||||
|
||||
OakEngineFootage *footage =
|
||||
oakengine_project_import_footage(project, media_path);
|
||||
assert(footage != NULL);
|
||||
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 10, 40, 5);
|
||||
assert(clip != NULL);
|
||||
|
||||
// Placing at in=10 on an empty track inserts a leading gap first, so the
|
||||
// event fires twice (gap 0..10, then the clip 10..40); the last
|
||||
// delivery is the clip itself.
|
||||
assert(log.count[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] == 2);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] ==
|
||||
(void *)clip);
|
||||
assert(log.last_a[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] == 10);
|
||||
assert(log.last_b[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] == 40);
|
||||
assert(log.last_source[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] ==
|
||||
(void *)track);
|
||||
assert(oakengine_event_unsubscribe(sub_block) == OAKENGINE_OK);
|
||||
oakengine_footage_free(footage);
|
||||
|
||||
// Marker added / modified.
|
||||
int64_t sub_marker_add = oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED, record_event, &log);
|
||||
int64_t sub_marker_mod = oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED, record_event, &log);
|
||||
assert(sub_marker_add > 0 && sub_marker_mod > 0);
|
||||
|
||||
assert(oakengine_sequence_marker_add(seq, 7, "Mark") == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED] == 7);
|
||||
|
||||
assert(oakengine_sequence_marker_rename(seq, 7, "Renamed") ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED] == 7);
|
||||
|
||||
assert(oakengine_event_unsubscribe(sub_marker_add) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_marker_mod) == OAKENGINE_OK);
|
||||
|
||||
// Workarea enabled + range changed.
|
||||
int64_t sub_range = oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED, record_event,
|
||||
&log);
|
||||
int64_t sub_enabled = oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED, record_event,
|
||||
&log);
|
||||
assert(sub_range > 0 && sub_enabled > 0);
|
||||
|
||||
assert(oakengine_sequence_set_workarea(seq, 1, 3, 21) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED] ==
|
||||
1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED] ==
|
||||
1);
|
||||
assert(log.count[OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED] == 3);
|
||||
assert(log.last_b[OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED] ==
|
||||
21);
|
||||
|
||||
assert(oakengine_event_unsubscribe(sub_range) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_enabled) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Track block traversal ---------------------------------------------------
|
||||
|
||||
static void test_block_traversal(OakEngineSequence *seq)
|
||||
{
|
||||
OakEngineTrack *track = oakengine_sequence_track_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0);
|
||||
assert(track != NULL);
|
||||
|
||||
// The caller placed one clip at 10..40; the track chain is
|
||||
// gap(0..10) -> clip(10..40).
|
||||
assert(oakengine_track_block_count(track) == 2);
|
||||
|
||||
OakEngineBlock *gap =
|
||||
oakengine_track_nearest_block_before_or_at(track, 0);
|
||||
assert(gap != NULL);
|
||||
assert(oakengine_block_is_gap(gap) == 1);
|
||||
|
||||
OakEngineBlock *clip = oakengine_block_next(gap);
|
||||
assert(clip != NULL);
|
||||
assert(oakengine_block_is_gap(clip) == 0);
|
||||
assert(oakengine_block_next(clip) == NULL);
|
||||
assert(oakengine_block_prev(clip) == gap);
|
||||
assert(oakengine_block_prev(gap) == NULL);
|
||||
|
||||
int64_t in = -1, out = -1;
|
||||
assert(oakengine_block_get_range(gap, &in, &out) == OAKENGINE_OK);
|
||||
assert(in == 0 && out == 10);
|
||||
assert(oakengine_block_get_range(clip, &in, &out) == OAKENGINE_OK);
|
||||
assert(in == 10 && out == 40);
|
||||
|
||||
// Time queries.
|
||||
assert(oakengine_track_block_at_time(track, 15) == clip);
|
||||
assert(oakengine_track_block_at_time(track, 5) == gap);
|
||||
assert(oakengine_track_block_at_time(track, 40) == NULL);
|
||||
assert(oakengine_track_nearest_block_before(track, 10) == gap);
|
||||
assert(oakengine_track_nearest_block_before_or_at(track, 10) == clip);
|
||||
assert(oakengine_track_nearest_block_after(track, 0) == clip);
|
||||
assert(oakengine_track_nearest_block_after_or_at(track, 10) == clip);
|
||||
assert(oakengine_track_nearest_block_after(track, 10) == NULL);
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_track_block_count(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_track_block_at_time(NULL, 0) == NULL);
|
||||
assert(oakengine_track_nearest_block_before(NULL, 0) == NULL);
|
||||
assert(oakengine_track_nearest_block_before_or_at(NULL, 0) == NULL);
|
||||
assert(oakengine_track_nearest_block_after(NULL, 0) == NULL);
|
||||
assert(oakengine_track_nearest_block_after_or_at(NULL, 0) == NULL);
|
||||
assert(oakengine_block_next(NULL) == NULL);
|
||||
assert(oakengine_block_prev(NULL) == NULL);
|
||||
assert(oakengine_block_is_gap(NULL) == 0);
|
||||
assert(oakengine_block_get_range(NULL, &in, &out) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
// ---- Node events (B8a) ------------------------------------------------------
|
||||
|
||||
static void test_node_events(OakEngineProject *project)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
OakEngineNode *lut = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.ociolut");
|
||||
OakEngineNode *text = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.text3");
|
||||
assert(solid != NULL && lut != NULL && text != NULL);
|
||||
|
||||
// Family mismatch: node events need a node handle.
|
||||
assert(oakengine_event_subscribe(
|
||||
project, OAKENGINE_EVENT_NODE_LABEL_CHANGED, record_event,
|
||||
&log) == 0);
|
||||
|
||||
int64_t subs[16];
|
||||
int nsubs = 0;
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_LABEL_CHANGED, record_event, &log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED, record_event, &log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
lut, OAKENGINE_EVENT_NODE_INPUT_CONNECTED, record_event, &log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
lut, OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED, record_event, &log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED, record_event,
|
||||
&log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
text, OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED, record_event,
|
||||
&log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED, record_event,
|
||||
&log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_KEYFRAME_ADDED, record_event, &log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED, record_event, &log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED, record_event,
|
||||
&log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED, record_event,
|
||||
&log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
solid, OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED, record_event,
|
||||
&log);
|
||||
for (int i = 0; i < nsubs; i++) {
|
||||
assert(subs[i] > 0);
|
||||
}
|
||||
|
||||
// Label.
|
||||
assert(oakengine_node_set_label(solid, "EventSolid") == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_LABEL_CHANGED] == 1);
|
||||
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_LABEL_CHANGED],
|
||||
"EventSolid") == 0);
|
||||
assert(log.last_source[OAKENGINE_EVENT_NODE_LABEL_CHANGED] == solid);
|
||||
|
||||
// Value change on an input: element -1, a valid range, the input id.
|
||||
oak_node_value v;
|
||||
memset(&v, 0, sizeof(v));
|
||||
v.type = OAK_NODE_VALUE_COLOR;
|
||||
v.f[0] = 0.5;
|
||||
v.f[3] = 1.0;
|
||||
assert(oakengine_node_set_input(solid, "color_in", &v) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED] >= 1);
|
||||
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED],
|
||||
"color_in") == 0);
|
||||
assert(log.last_a[OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED] == -1);
|
||||
|
||||
// Edge connect/disconnect: output node in the handle field.
|
||||
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_CONNECTED] == 1);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_NODE_INPUT_CONNECTED] == solid);
|
||||
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_INPUT_CONNECTED],
|
||||
"tex_in") == 0);
|
||||
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED] == 1);
|
||||
|
||||
// Property change (notified write only).
|
||||
assert(oakengine_node_set_input_property_string(solid, "color_in",
|
||||
"my_prop", "1", 1) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED] == 1);
|
||||
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED],
|
||||
"color_in") == 0);
|
||||
assert(oakengine_node_set_input_property_string(solid, "color_in",
|
||||
"my_prop", "2", 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED] == 1);
|
||||
|
||||
// Array size change: old and new sizes.
|
||||
const int arr_before = oakengine_node_input_array_size(text, "args_in");
|
||||
assert(oakengine_node_array_insert_at(text, "args_in", 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED] ==
|
||||
arr_before);
|
||||
assert(log.last_b[OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED] ==
|
||||
arr_before + 1);
|
||||
|
||||
// Keyframe enable + add/remove/time/type/value.
|
||||
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_ADDED);
|
||||
assert(oakengine_node_set_input_keyframing(solid, "color_in", -1, 1, 0,
|
||||
1, NULL) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED] == 1);
|
||||
assert(log.last_b[OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED] == 1);
|
||||
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED],
|
||||
"color_in") == 0);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_ADDED] >= 1);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_NODE_KEYFRAME_ADDED] != NULL);
|
||||
// COLOR has four tracks; enabling keyframing adds one key per track.
|
||||
assert(log.last_b[OAKENGINE_EVENT_NODE_KEYFRAME_ADDED] == 3);
|
||||
|
||||
// Type change on the created key (ts 0 = time 0s).
|
||||
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED);
|
||||
int64_t times[1] = { 0 };
|
||||
int tracks[1] = { 0 };
|
||||
// The engine only emits the type-changed signal on multi-key tracks,
|
||||
// so add a second key (1s = ts 30 with the default timebase) first.
|
||||
assert(oakengine_node_keyframes_toggle_at_time(solid, "color_in", -1, 1,
|
||||
1, 1, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
// Default type is bezier; switch to hold (type 2) for a real change.
|
||||
assert(oakengine_node_keyframes_set_type_many(solid, "color_in", -1,
|
||||
times, tracks, 1, 2) == 1);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED] == 1);
|
||||
|
||||
// Value change.
|
||||
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED);
|
||||
oak_node_value nv;
|
||||
memset(&nv, 0, sizeof(nv));
|
||||
nv.type = OAK_NODE_VALUE_COLOR;
|
||||
nv.f[0] = 0.75;
|
||||
assert(oakengine_node_keyframes_set_value_many(solid, "color_in", -1,
|
||||
times, tracks, 1, &nv,
|
||||
NULL) == 1);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED] == 1);
|
||||
|
||||
// Time change.
|
||||
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED);
|
||||
assert(oakengine_node_keyframes_set_time_many(solid, "color_in", -1,
|
||||
times, tracks, 1,
|
||||
30) == 1);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED] == 1);
|
||||
|
||||
// Removal.
|
||||
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED);
|
||||
assert(oakengine_node_set_input_keyframing(solid, "color_in", -1, 0, 0,
|
||||
1, NULL) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED] >= 1);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED] == 2);
|
||||
|
||||
for (int i = 0; i < nsubs; i++) {
|
||||
assert(oakengine_event_unsubscribe(subs[i]) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, text) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Group + context position events -----------------------------------------
|
||||
|
||||
static void test_group_events(OakEngineProject *project)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
OakEngineNode *group = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.group");
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(group != NULL && solid != NULL);
|
||||
|
||||
int64_t subs[4];
|
||||
int nsubs = 0;
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
group, OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED, record_event,
|
||||
&log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
group, OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED, record_event,
|
||||
&log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
group, OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED, record_event,
|
||||
&log);
|
||||
subs[nsubs++] = oakengine_event_subscribe(
|
||||
group, OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED, record_event,
|
||||
&log);
|
||||
for (int i = 0; i < nsubs; i++) {
|
||||
assert(subs[i] > 0);
|
||||
}
|
||||
|
||||
// The group must contain the inner node before a passthrough can be
|
||||
// added (insertion itself fires the position-changed event).
|
||||
assert(oakengine_node_set_context_position(group, solid, 0.0, 0.0) ==
|
||||
OAKENGINE_OK);
|
||||
reset_event(&log, OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED);
|
||||
|
||||
// Add passthrough: handle = inner node, s = input id, a = element.
|
||||
assert(oakengine_group_add_input_passthrough(group, solid, "color_in",
|
||||
-1, NULL, NULL, 0) > 0);
|
||||
assert(log.count[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED] == 1);
|
||||
assert(log.last_source[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED] ==
|
||||
group);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED] ==
|
||||
solid);
|
||||
assert(strcmp(log.last_s[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED],
|
||||
"color_in") == 0);
|
||||
assert(log.last_a[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED] == -1);
|
||||
|
||||
// Output passthrough: handle = the new output node.
|
||||
assert(oakengine_group_set_output_passthrough(group, solid) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED] == 1);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED] ==
|
||||
solid);
|
||||
|
||||
// Remove passthrough.
|
||||
assert(oakengine_group_remove_input_passthrough(group, solid, "color_in",
|
||||
-1) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED] == 1);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED] ==
|
||||
solid);
|
||||
assert(strcmp(log.last_s[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED],
|
||||
"color_in") == 0);
|
||||
assert(log.last_a[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED] == -1);
|
||||
|
||||
// Context position: handle = child node, a/b = x/y double bit patterns.
|
||||
assert(oakengine_node_set_context_position(group, solid, 3.5, -2.25) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED] == 1);
|
||||
assert(log.last_source[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED] ==
|
||||
group);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED] ==
|
||||
solid);
|
||||
double px, py;
|
||||
memcpy(&px, &log.last_a[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED],
|
||||
sizeof(px));
|
||||
memcpy(&py, &log.last_b[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED],
|
||||
sizeof(py));
|
||||
assert(px == 3.5 && py == -2.25);
|
||||
|
||||
// Moving again re-emits with the new coordinates.
|
||||
assert(oakengine_node_set_context_position(group, solid, 0.0, 1.0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED] == 2);
|
||||
memcpy(&px, &log.last_a[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED],
|
||||
sizeof(px));
|
||||
memcpy(&py, &log.last_b[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED],
|
||||
sizeof(py));
|
||||
assert(px == 0.0 && py == 1.0);
|
||||
|
||||
for (int i = 0; i < nsubs; i++) {
|
||||
assert(oakengine_event_unsubscribe(subs[i]) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
assert(oakengine_project_remove_node(project, group) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Track / block state events (B4c) ---------------------------------------
|
||||
|
||||
// The caller left one clip at 10..40 on video track 0 (see
|
||||
// test_sequence_events); `track` is that track.
|
||||
static void test_track_extra_events(OakEngineSequence *seq,
|
||||
OakEngineTrack *track)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
// Family mismatches: a track is not a sequence and vice versa.
|
||||
assert(oakengine_event_subscribe(
|
||||
track, OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED,
|
||||
record_event, &log) == 0);
|
||||
assert(oakengine_event_subscribe(seq, OAKENGINE_EVENT_TRACK_MUTED_CHANGED,
|
||||
record_event, &log) == 0);
|
||||
|
||||
// Muted changed.
|
||||
int64_t sub_mute = oakengine_event_subscribe(
|
||||
track, OAKENGINE_EVENT_TRACK_MUTED_CHANGED, record_event, &log);
|
||||
assert(sub_mute > 0);
|
||||
assert(oakengine_track_set_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_TRACK_MUTED_CHANGED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_TRACK_MUTED_CHANGED] == 1);
|
||||
assert(oakengine_track_set_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_TRACK_MUTED_CHANGED] == 2);
|
||||
assert(log.last_a[OAKENGINE_EVENT_TRACK_MUTED_CHANGED] == 0);
|
||||
assert(oakengine_event_unsubscribe(sub_mute) == OAKENGINE_OK);
|
||||
|
||||
// Track height changed (track-level, double bit pattern) and the
|
||||
// sequence-level pixel variant.
|
||||
int64_t sub_h = oakengine_event_subscribe(
|
||||
track, OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED, record_event, &log);
|
||||
int64_t sub_sh = oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED, record_event,
|
||||
&log);
|
||||
assert(sub_h > 0 && sub_sh > 0);
|
||||
assert(oakengine_track_set_height(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
2.5) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED] == 1);
|
||||
double h;
|
||||
memcpy(&h, &log.last_a[OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED], sizeof(h));
|
||||
assert(h == 2.5);
|
||||
assert(log.count[OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED] ==
|
||||
OAKENGINE_TRACK_TYPE_VIDEO);
|
||||
assert(log.last_b[OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED] ==
|
||||
oakengine_track_height_internal_to_pixels(2.5));
|
||||
assert(log.last_handle[OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED] ==
|
||||
(void *)track);
|
||||
assert(oakengine_event_unsubscribe(sub_h) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_sh) == OAKENGINE_OK);
|
||||
|
||||
// Track list changed + index changed: append a second video track,
|
||||
// then move track 0 to position 1.
|
||||
int64_t sub_list = oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED, record_event, &log);
|
||||
assert(sub_list > 0);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
1);
|
||||
assert(log.count[OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED] >= 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED] ==
|
||||
OAKENGINE_TRACK_TYPE_VIDEO);
|
||||
|
||||
int64_t sub_index = oakengine_event_subscribe(
|
||||
track, OAKENGINE_EVENT_TRACK_INDEX_CHANGED, record_event, &log);
|
||||
assert(sub_index > 0);
|
||||
assert(oakengine_sequence_move_track(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
1) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_TRACK_INDEX_CHANGED] >= 1);
|
||||
assert(log.last_b[OAKENGINE_EVENT_TRACK_INDEX_CHANGED] == 1);
|
||||
assert(oakengine_sequence_move_track(seq, OAKENGINE_TRACK_TYPE_VIDEO, 1,
|
||||
0) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_index) == OAKENGINE_OK);
|
||||
|
||||
// Clean up the extra track.
|
||||
assert(oakengine_sequence_remove_track(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
1) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_list) == OAKENGINE_OK);
|
||||
|
||||
// Blocks refreshed: emitted when the track re-lays out its block chain
|
||||
// (e.g. moving a clip onto it).
|
||||
int64_t sub_refresh = oakengine_event_subscribe(
|
||||
track, OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED, record_event, &log);
|
||||
assert(sub_refresh > 0);
|
||||
assert(oakengine_sequence_move_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0,
|
||||
50) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED] >= 1);
|
||||
// Move it back to 10 to restore the original layout.
|
||||
assert(oakengine_sequence_move_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0,
|
||||
10) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_refresh) == OAKENGINE_OK);
|
||||
|
||||
// Subtitles changed: subscription validates (no headless trigger --
|
||||
// the signal only fires on subtitle-track cache invalidation).
|
||||
int64_t sub_subs = oakengine_event_subscribe(
|
||||
seq, OAKENGINE_EVENT_SEQUENCE_SUBTITLES_CHANGED, record_event, &log);
|
||||
assert(sub_subs > 0);
|
||||
assert(oakengine_event_unsubscribe(sub_subs) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_block_state_events(OakEngineSequence *seq,
|
||||
const char *media_path)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
// The clip is back at 10..40 (clip index 0; the leading gap is not
|
||||
// counted by the clip family).
|
||||
OakEngineClip *clip = oakengine_sequence_clip_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
|
||||
assert(clip != NULL);
|
||||
OakEngineBlock *block = (OakEngineBlock *)clip;
|
||||
|
||||
// Family mismatch: a block is not a track.
|
||||
assert(oakengine_event_subscribe(
|
||||
block, OAKENGINE_EVENT_TRACK_MUTED_CHANGED, record_event,
|
||||
&log) == 0);
|
||||
|
||||
int64_t sub_en = oakengine_event_subscribe(
|
||||
block, OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED, record_event, &log);
|
||||
assert(sub_en > 0);
|
||||
// The engine emits enabled_changed twice per flip (Block::set_enabled
|
||||
// and Block::InputValueChangedEvent), so each toggle delivers two.
|
||||
OakEngineClip *clips[1] = { clip };
|
||||
assert(oakengine_clip_toggle_enabled(clips, 1) == 1);
|
||||
assert(log.count[OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED] == 2);
|
||||
assert(oakengine_block_is_enabled(block) == 0);
|
||||
assert(oakengine_clip_toggle_enabled(clips, 1) == 1);
|
||||
assert(log.count[OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED] == 4);
|
||||
assert(oakengine_block_is_enabled(block) == 1);
|
||||
assert(oakengine_event_unsubscribe(sub_en) == OAKENGINE_OK);
|
||||
|
||||
// Preview changed: writing the loop-mode input fires it.
|
||||
int64_t sub_prev = oakengine_event_subscribe(
|
||||
block, OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED, record_event, &log);
|
||||
assert(sub_prev > 0);
|
||||
oak_node_value v;
|
||||
memset(&v, 0, sizeof(v));
|
||||
v.type = OAK_NODE_VALUE_COMBO;
|
||||
v.num = 1;
|
||||
assert(oakengine_node_set_input((OakEngineNode *)clip,
|
||||
oakengine_clip_loop_mode_input_id(),
|
||||
&v) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED] == 1);
|
||||
v.num = 0;
|
||||
assert(oakengine_node_set_input((OakEngineNode *)clip,
|
||||
oakengine_clip_loop_mode_input_id(),
|
||||
&v) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED] == 2);
|
||||
assert(oakengine_event_unsubscribe(sub_prev) == OAKENGINE_OK);
|
||||
|
||||
// Node links/color changed (Node signals on the block handle).
|
||||
int64_t sub_links = oakengine_event_subscribe(
|
||||
(OakEngineNode *)clip, OAKENGINE_EVENT_NODE_LINKS_CHANGED,
|
||||
record_event, &log);
|
||||
int64_t sub_color = oakengine_event_subscribe(
|
||||
(OakEngineNode *)clip, OAKENGINE_EVENT_NODE_COLOR_CHANGED,
|
||||
record_event, &log);
|
||||
assert(sub_links > 0 && sub_color > 0);
|
||||
OakEngineNode *one[1] = { (OakEngineNode *)clip };
|
||||
assert(oakengine_node_set_color_label(one, 1, 4) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_COLOR_CHANGED] == 1);
|
||||
OakEngineFootage *footage =
|
||||
oakengine_project_import_footage(oakengine_node_get_project(
|
||||
(OakEngineNode *)seq),
|
||||
media_path);
|
||||
assert(footage != NULL);
|
||||
OakEngineClip *clip2 = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 50, 80, 0);
|
||||
assert(clip2 != NULL);
|
||||
OakEngineClip *pair[2] = { clip, clip2 };
|
||||
assert(oakengine_clip_set_linked(pair, 2, 1) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_NODE_LINKS_CHANGED] >= 1);
|
||||
assert(oakengine_clip_set_linked(pair, 2, 0) == OAKENGINE_OK);
|
||||
oakengine_footage_free(footage);
|
||||
assert(oakengine_event_unsubscribe(sub_links) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_color) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_marker_list_events(OakEngineSequence *seq)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
OakEngineMarkerList *list =
|
||||
oakengine_viewer_get_marker_list((OakEngineNode *)seq);
|
||||
assert(list != NULL);
|
||||
|
||||
// Family mismatch: a marker list is not a workarea.
|
||||
assert(oakengine_event_subscribe(
|
||||
list, OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED, record_event,
|
||||
&log) == 0);
|
||||
|
||||
int64_t sub_add = oakengine_event_subscribe(
|
||||
list, OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED, record_event, &log);
|
||||
int64_t sub_mod = oakengine_event_subscribe(
|
||||
list, OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED, record_event,
|
||||
&log);
|
||||
int64_t sub_rm = oakengine_event_subscribe(
|
||||
list, OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED, record_event,
|
||||
&log);
|
||||
assert(sub_add > 0 && sub_mod > 0 && sub_rm > 0);
|
||||
|
||||
// Add a marker at 2 seconds (rational seconds, not timestamps).
|
||||
assert(oakengine_marker_list_add(list, 2, 1, 2, 1, "ListMark", 3) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED] == 1);
|
||||
OakEngineMarker *marker = (OakEngineMarker *)log.last_handle
|
||||
[OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED];
|
||||
assert(marker != NULL);
|
||||
assert(oakengine_marker_list_at(list, 0) != NULL);
|
||||
|
||||
// Modify: recolor through the properties batch.
|
||||
OakEngineMarker *one[1] = { marker };
|
||||
assert(oakengine_marker_set_properties(one, 1, 5, NULL, 0, 0, 0, 0, 0,
|
||||
NULL) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED] == 1);
|
||||
assert(log.last_handle[OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED] ==
|
||||
(void *)marker);
|
||||
|
||||
// Remove.
|
||||
assert(oakengine_marker_remove(marker) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED] == 1);
|
||||
|
||||
assert(oakengine_event_unsubscribe(sub_add) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_mod) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_rm) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_workarea_events(OakEngineSequence *seq)
|
||||
{
|
||||
EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
|
||||
// Viewer-owned (borrowed) workarea.
|
||||
OakEngineWorkarea *wa =
|
||||
oakengine_viewer_get_workarea_handle((OakEngineNode *)seq);
|
||||
assert(wa != NULL);
|
||||
|
||||
int64_t sub_range = oakengine_event_subscribe(
|
||||
wa, OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED, record_event, &log);
|
||||
int64_t sub_en = oakengine_event_subscribe(
|
||||
wa, OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED, record_event, &log);
|
||||
assert(sub_range > 0 && sub_en > 0);
|
||||
|
||||
assert(oakengine_workarea_set_range(wa, 1, 1, 4, 1) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED] == 1);
|
||||
int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0;
|
||||
assert(oakengine_workarea_get(wa, &in_num, &in_den, &out_num, &out_den,
|
||||
NULL) == OAKENGINE_OK);
|
||||
assert(in_num == 1 && in_den == 1 && out_num == 4 && out_den == 1);
|
||||
|
||||
assert(oakengine_workarea_set_enabled(wa, 1) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED] == 1);
|
||||
assert(log.last_a[OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED] == 1);
|
||||
assert(oakengine_workarea_set_enabled(wa, 0) == OAKENGINE_OK);
|
||||
|
||||
assert(oakengine_event_unsubscribe(sub_range) == OAKENGINE_OK);
|
||||
assert(oakengine_event_unsubscribe(sub_en) == OAKENGINE_OK);
|
||||
|
||||
// Standalone owned workarea (the footage viewer override pattern).
|
||||
OakEngineWorkarea *over = oakengine_workarea_create();
|
||||
assert(over != NULL);
|
||||
int64_t sub_over = oakengine_event_subscribe(
|
||||
over, OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED, record_event, &log);
|
||||
assert(sub_over > 0);
|
||||
assert(oakengine_workarea_set_range(over, 0, 1, 7, 2) == OAKENGINE_OK);
|
||||
assert(log.count[OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED] == 2);
|
||||
assert(oakengine_event_unsubscribe(sub_over) == OAKENGINE_OK);
|
||||
oakengine_workarea_free(over);
|
||||
oakengine_workarea_free(NULL);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
|
||||
// Sandbox the config/cache/data locations (see oakengine_init_test).
|
||||
#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
|
||||
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Events");
|
||||
assert(seq != NULL);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
0);
|
||||
|
||||
char path[4096];
|
||||
demo_path(path, sizeof(path));
|
||||
|
||||
OakEngineTrack *track = oakengine_sequence_track_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0);
|
||||
assert(track != NULL);
|
||||
|
||||
test_subscribe_validation(project, seq, track);
|
||||
test_project_events(project);
|
||||
test_folder_events(project);
|
||||
test_sequence_events(project, seq, path);
|
||||
test_block_traversal(seq);
|
||||
test_track_extra_events(seq, track);
|
||||
test_block_state_events(seq, path);
|
||||
test_marker_list_events(seq);
|
||||
test_workarea_events(seq);
|
||||
test_node_events(project);
|
||||
test_group_events(project);
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
printf("oakengine_events_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -424,6 +424,10 @@ int main(void)
|
||||
// ---- render_ex: real exports ---------------------------------------------
|
||||
{
|
||||
// H.264 + AAC over a custom 20-frame range of the same sequence.
|
||||
// Exercise the encoder-specific video option pass-through
|
||||
// (crf=18) for this render, then clear it so later exports and
|
||||
// the cancellation re-run are unaffected.
|
||||
oakengine_export_set_video_option("crf", "18");
|
||||
char out3[4096];
|
||||
snprintf(out3, sizeof(out3), "%s/ex_custom.mp4", g_tmpdir);
|
||||
oak_export_options_ex o3;
|
||||
@@ -448,6 +452,7 @@ int main(void)
|
||||
"(no error)");
|
||||
}
|
||||
assert(rc == OAKENGINE_OK);
|
||||
oakengine_export_set_video_option(NULL, NULL);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"ffprobe -v error -show_entries stream=codec_type,duration "
|
||||
"-of csv=p=0 \"%s\"",
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
|
||||
#include "oakengine/footage.h"
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/node.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/timeline.h"
|
||||
|
||||
#ifndef OAK_TEST_SOURCE_DIR
|
||||
#define OAK_TEST_SOURCE_DIR "."
|
||||
@@ -588,6 +590,293 @@ static void test_colorspace_candidates(void)
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// Project extras: filenames, cache paths, settings, MIME type, from_object.
|
||||
static void test_project_extras(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
char buf[4096];
|
||||
|
||||
// Untitled project: pretty filename is the "(untitled)" placeholder.
|
||||
assert(oakengine_project_pretty_filename(project, buf, sizeof(buf)) > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
|
||||
// set_filename round-trips through the plain filename getter.
|
||||
char target[4096];
|
||||
snprintf(target, sizeof(target), "%s/roundtrip.ove", g_tmpdir);
|
||||
assert(oakengine_project_set_filename(project, target) == OAKENGINE_OK);
|
||||
assert(oakengine_project_filename(project, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, target) == 0);
|
||||
assert(oakengine_project_set_filename(project, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_project_set_filename(NULL, target) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// With a filename set, the cache paths are derivable and non-empty.
|
||||
assert(oakengine_project_cache_path(project, buf, sizeof(buf)) > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
assert(oakengine_project_cache_alongside_path(project, buf, sizeof(buf)) >
|
||||
0);
|
||||
assert(strlen(buf) > 0);
|
||||
|
||||
// Custom cache path setting round-trip (NULL clears).
|
||||
assert(oakengine_project_set_custom_cache_path(project, "/tmp/oakcache") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_project_get_custom_cache_path(project, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "/tmp/oakcache") == 0);
|
||||
assert(oakengine_project_set_custom_cache_path(project, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_project_get_custom_cache_path(project, buf,
|
||||
sizeof(buf)) == 0);
|
||||
|
||||
// Color reference space setting round-trip.
|
||||
assert(oakengine_project_set_color_reference_space(
|
||||
project, "Rec.709 OETF") == OAKENGINE_OK);
|
||||
assert(oakengine_project_get_color_reference_space(project, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "Rec.709 OETF") == 0);
|
||||
assert(oakengine_project_set_color_reference_space(NULL, "x") ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Cache location setting defaults to a valid enum value; NULL is invalid.
|
||||
assert(oakengine_project_get_cache_location_setting(project) >= 0);
|
||||
assert(oakengine_project_get_cache_location_setting(NULL) < 0);
|
||||
|
||||
// The project item MIME type is a non-empty static string.
|
||||
const char *mime = oakengine_project_item_mime_type();
|
||||
assert(mime != NULL && strlen(mime) > 0);
|
||||
|
||||
// from_object: the root node resolves back to its owning project.
|
||||
OakEngineNode *root = oakengine_project_node_at(project, 0);
|
||||
assert(root != NULL);
|
||||
assert(oakengine_project_from_object(root) == project);
|
||||
assert(oakengine_project_from_object(NULL) == NULL);
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_project_pretty_filename(NULL, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_project_cache_path(NULL, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_project_get_custom_cache_path(NULL, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_project_get_color_reference_space(NULL, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// Folder creation and child queries.
|
||||
static void test_folder(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
// A fresh project's first node is its root folder.
|
||||
OakEngineNode *root = oakengine_project_node_at(project, 0);
|
||||
assert(root != NULL);
|
||||
|
||||
OakEngineNode *folder =
|
||||
oakengine_folder_create(project, root, "My Folder");
|
||||
assert(folder != NULL);
|
||||
assert(oakengine_folder_has_child_recursive(root, folder) == 1);
|
||||
assert(oakengine_folder_index_of_child(root, folder) >= 0);
|
||||
|
||||
// A subfolder is found recursively from the root.
|
||||
OakEngineNode *sub = oakengine_folder_create(project, folder, "Sub");
|
||||
assert(sub != NULL);
|
||||
assert(oakengine_folder_has_child_recursive(root, sub) == 1);
|
||||
assert(oakengine_folder_has_child_recursive(folder, sub) == 1);
|
||||
assert(oakengine_folder_has_child_recursive(sub, folder) == 0);
|
||||
|
||||
// A folder from another project is not a child here.
|
||||
OakEngineProject *other = oakengine_project_create();
|
||||
assert(other != NULL);
|
||||
assert(oakengine_project_new(other) == OAKENGINE_OK);
|
||||
OakEngineNode *other_root = oakengine_project_node_at(other, 0);
|
||||
assert(other_root != NULL);
|
||||
OakEngineNode *alien = oakengine_folder_create(other, other_root, "Alien");
|
||||
assert(alien != NULL);
|
||||
assert(oakengine_folder_has_child_recursive(root, alien) == 0);
|
||||
assert(oakengine_folder_index_of_child(root, alien) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
oakengine_project_free(other);
|
||||
|
||||
// The child input key is a non-empty static string.
|
||||
const char *key = oakengine_folder_child_input_key();
|
||||
assert(key != NULL && strlen(key) > 0);
|
||||
|
||||
// Error paths: non-folder parents, non-folder queries, NULL.
|
||||
assert(oakengine_folder_create(project, folder, NULL) != NULL);
|
||||
OakEngineNode *footage_node = NULL;
|
||||
{
|
||||
char path[4096];
|
||||
demo_path(path, sizeof(path));
|
||||
OakEngineFootage *f = oakengine_project_import_footage(project, path);
|
||||
assert(f != NULL);
|
||||
oakengine_footage_free(f);
|
||||
// The imported footage is a non-folder project node.
|
||||
for (int i = 0; i < oakengine_project_node_count(project); i++) {
|
||||
OakEngineNode *n = oakengine_project_node_at(project, i);
|
||||
char id[128];
|
||||
assert(oakengine_node_get_type_id(n, id, sizeof(id)) > 0);
|
||||
if (strcmp(id, "org.olivevideoeditor.Olive.folder") != 0) {
|
||||
footage_node = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(footage_node != NULL);
|
||||
}
|
||||
assert(oakengine_folder_create(project, footage_node, "Nope") == NULL);
|
||||
assert(oakengine_folder_has_child_recursive(footage_node, folder) == 0);
|
||||
assert(oakengine_folder_index_of_child(footage_node, folder) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_folder_has_child_recursive(NULL, folder) == 0);
|
||||
assert(oakengine_folder_has_child_recursive(root, NULL) == 0);
|
||||
assert(oakengine_folder_index_of_child(NULL, folder) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_folder_index_of_child(root, NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_folder_create(NULL, root, "Nope") == NULL);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// Footage extras: filename, stream references, descriptions, proxy params,
|
||||
// manual proxy state and invalidation.
|
||||
static void test_footage_extras(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
char path[4096];
|
||||
demo_path(path, sizeof(path));
|
||||
OakEngineFootage *f = oakengine_project_import_footage(project, path);
|
||||
assert(f != NULL);
|
||||
|
||||
char buf[4096];
|
||||
|
||||
// Filename of the imported footage.
|
||||
assert(oakengine_footage_get_filename(f, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, path) == 0);
|
||||
assert(oakengine_footage_get_filename(NULL, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Real stream index 0 is the video stream, 1 the audio stream.
|
||||
int track_type = -1, stream_index = -1;
|
||||
assert(oakengine_footage_get_stream_reference(f, 0, &track_type,
|
||||
&stream_index) == OAKENGINE_OK);
|
||||
assert(track_type == OAKENGINE_TRACK_TYPE_VIDEO && stream_index == 0);
|
||||
assert(oakengine_footage_get_stream_reference(f, 1, &track_type,
|
||||
&stream_index) == OAKENGINE_OK);
|
||||
assert(track_type == OAKENGINE_TRACK_TYPE_AUDIO && stream_index == 0);
|
||||
assert(oakengine_footage_get_stream_reference(f, 99, &track_type,
|
||||
&stream_index) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_get_stream_reference(NULL, 0, &track_type,
|
||||
&stream_index) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Stream descriptions.
|
||||
assert(oakengine_footage_describe_video_stream(f, 0, buf, sizeof(buf)) >
|
||||
0);
|
||||
assert(strlen(buf) > 0);
|
||||
assert(oakengine_footage_describe_audio_stream(f, 0, buf, sizeof(buf)) >
|
||||
0);
|
||||
assert(strlen(buf) > 0);
|
||||
assert(oakengine_footage_describe_video_stream(f, 9, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_describe_audio_stream(f, 9, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_describe_video_stream(NULL, 0, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Static stream type names (no handle needed).
|
||||
assert(oakengine_footage_stream_type_name(OAKENGINE_TRACK_TYPE_VIDEO, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
assert(oakengine_footage_stream_type_name(OAKENGINE_TRACK_TYPE_AUDIO, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
|
||||
// Proxy params: effective defaults first, then a custom round-trip.
|
||||
assert(oakengine_footage_has_custom_proxy_params(f) == 0);
|
||||
oak_proxy_params params;
|
||||
memset(¶ms, 0, sizeof(params));
|
||||
assert(oakengine_footage_get_effective_proxy_params(f, ¶ms) ==
|
||||
OAKENGINE_OK);
|
||||
assert(params.width > 0 && params.height > 0);
|
||||
params.width = 640;
|
||||
params.height = 360;
|
||||
params.divider = 1;
|
||||
params.version = 1;
|
||||
params.crf = 30;
|
||||
params.include_audio = 0;
|
||||
strcpy(params.extension, "mkv");
|
||||
strcpy(params.preset, "slow");
|
||||
assert(oakengine_footage_set_custom_proxy_params(f, ¶ms) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_footage_has_custom_proxy_params(f) == 1);
|
||||
oak_proxy_params back;
|
||||
memset(&back, 0, sizeof(back));
|
||||
assert(oakengine_footage_get_effective_proxy_params(f, &back) ==
|
||||
OAKENGINE_OK);
|
||||
assert(back.width == 640 && back.height == 360 && back.crf == 30);
|
||||
assert(back.include_audio == 0);
|
||||
assert(strcmp(back.extension, "mkv") == 0);
|
||||
assert(strcmp(back.preset, "slow") == 0);
|
||||
assert(oakengine_footage_clear_custom_proxy_params(f) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_has_custom_proxy_params(f) == 0);
|
||||
assert(oakengine_footage_set_custom_proxy_params(f, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_get_effective_proxy_params(f, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_has_custom_proxy_params(NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Manual proxy state: set then clear (no file is created here).
|
||||
assert(oakengine_footage_set_proxy(f, "/tmp/fake_proxy.mp4", 2, 0, 1,
|
||||
1) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_proxy_get_state(f) == 2);
|
||||
assert(oakengine_footage_proxy_get_path(f, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "/tmp/fake_proxy.mp4") == 0);
|
||||
assert(oakengine_footage_proxy_is_enabled(f) == 1);
|
||||
assert(oakengine_footage_clear_proxy(f) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_proxy_get_state(f) == 0);
|
||||
assert(oakengine_footage_proxy_get_path(f, buf, sizeof(buf)) == 0);
|
||||
assert(oakengine_footage_set_proxy(NULL, "x", 2, 0, 1, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_clear_proxy(NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
// Cache invalidation after proxy/relink changes.
|
||||
assert(oakengine_footage_invalidate(f) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_invalidate(NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
// Probe handles carry no project node: the whole section rejects them.
|
||||
OakEngineFootage *probed = oakengine_footage_probe(path);
|
||||
assert(probed != NULL);
|
||||
assert(oakengine_footage_get_filename(probed, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_get_stream_reference(probed, 0, &track_type,
|
||||
&stream_index) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_describe_video_stream(probed, 0, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_get_effective_proxy_params(probed, ¶ms) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_invalidate(probed) == OAKENGINE_E_INVALID);
|
||||
oakengine_footage_free(probed);
|
||||
|
||||
oakengine_footage_free(f);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
@@ -610,6 +899,9 @@ int main(void)
|
||||
test_proxy();
|
||||
test_stream_overrides();
|
||||
test_colorspace_candidates();
|
||||
test_project_extras();
|
||||
test_folder();
|
||||
test_footage_extras();
|
||||
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
|
||||
@@ -562,6 +562,265 @@ static void test_keyframe_properties(OakEngineProject *project,
|
||||
0.f) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
// Handle-based keyframe family (B8a): enumeration, navigation, handle
|
||||
// accessors, live mutation, undoable batch operations, detached
|
||||
// create/paste/dispose and the input dragger.
|
||||
static void test_handle_family(OakEngineProject *project,
|
||||
OakEngineNode *opacity)
|
||||
{
|
||||
char buf[256];
|
||||
oak_node_value v;
|
||||
|
||||
// Start from a clean, keyframing-enabled, empty input.
|
||||
assert(oakengine_node_keyframes_clear(opacity, "opacity_in") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 0);
|
||||
|
||||
// Toggle ON at 1s: one keyframe with the current value and best type.
|
||||
assert(oakengine_node_keyframes_toggle_at_time(
|
||||
opacity, "opacity_in", -1, 1, 1, 1, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_track_count(opacity, "opacity_in", -1) ==
|
||||
1);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 1);
|
||||
assert(oakengine_node_has_keyframe_at_time(opacity, "opacity_in", -1, 1,
|
||||
1) == 1);
|
||||
// Toggling on again at the same time is a no-op.
|
||||
assert(oakengine_node_keyframes_toggle_at_time(
|
||||
opacity, "opacity_in", -1, 1, 1, 1, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 1);
|
||||
|
||||
// More keys via toggles for navigation tests.
|
||||
assert(oakengine_node_keyframes_toggle_at_time(
|
||||
opacity, "opacity_in", -1, 0, 1, 1, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframes_toggle_at_time(
|
||||
opacity, "opacity_in", -1, 3, 1, 1, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 3);
|
||||
|
||||
// Navigation.
|
||||
int64_t num = -1, den = -1;
|
||||
assert(oakengine_node_keyframe_earliest_time(opacity, "opacity_in", -1,
|
||||
&num, &den) == 1);
|
||||
assert(num == 0 && den == 1);
|
||||
assert(oakengine_node_keyframe_latest_time(opacity, "opacity_in", -1,
|
||||
&num, &den) == 1);
|
||||
assert(num == 3 && den == 1);
|
||||
assert(oakengine_node_keyframe_closest_time_before(
|
||||
opacity, "opacity_in", -1, 2, 1, &num, &den) == 1);
|
||||
assert(num == 1 && den == 1);
|
||||
assert(oakengine_node_keyframe_closest_time_after(
|
||||
opacity, "opacity_in", -1, 2, 1, &num, &den) == 1);
|
||||
assert(num == 3 && den == 1);
|
||||
assert(oakengine_node_keyframe_closest_time_before(
|
||||
opacity, "opacity_in", -1, 0, 1, &num, &den) == 0);
|
||||
assert(oakengine_node_keyframe_closest_time_after(
|
||||
opacity, "opacity_in", -1, 3, 1, &num, &den) == 0);
|
||||
|
||||
// Handle lookup: on-track enumeration, at-time lookup, and the batch
|
||||
// at-time query all agree.
|
||||
OakEngineKeyframe *k0 =
|
||||
oakengine_node_keyframe_handle_on_track(opacity, "opacity_in", -1, 0, 0);
|
||||
OakEngineKeyframe *k1 =
|
||||
oakengine_node_keyframe_handle_on_track(opacity, "opacity_in", -1, 0, 1);
|
||||
assert(k0 != NULL && k1 != NULL && k0 != k1);
|
||||
assert(oakengine_node_keyframe_handle_on_track(opacity, "opacity_in", -1,
|
||||
0, 3) == NULL);
|
||||
assert(oakengine_node_keyframe_handle_on_track(opacity, "opacity_in", -1,
|
||||
1, 0) == NULL);
|
||||
assert(oakengine_node_keyframe_handle_at_time(opacity, "opacity_in", -1,
|
||||
0, 0, 1) == k0);
|
||||
assert(oakengine_node_keyframe_handle_at_time(opacity, "opacity_in", -1,
|
||||
0, 1, 1) == k1);
|
||||
assert(oakengine_node_keyframe_handle_at_time(opacity, "opacity_in", -1,
|
||||
0, 2, 1) == NULL);
|
||||
OakEngineKeyframe *at[4] = { NULL, NULL, NULL, NULL };
|
||||
assert(oakengine_node_keyframes_at_time(opacity, "opacity_in", -1, 1, 1,
|
||||
at, 4) == 1);
|
||||
assert(at[0] == k1);
|
||||
|
||||
// Handle accessors.
|
||||
assert(oakengine_keyframe_get_time(k1, &num, &den) == OAKENGINE_OK);
|
||||
assert(num == 1 && den == 1);
|
||||
assert(oakengine_keyframe_get_input_id(k1, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "opacity_in") == 0);
|
||||
assert(oakengine_keyframe_get_track(k1) == 0);
|
||||
assert(oakengine_keyframe_get_element(k1) == -1);
|
||||
assert(oakengine_keyframe_get_node(k1) == opacity);
|
||||
assert(oakengine_keyframe_get_type(k1) >= 0);
|
||||
assert(oakengine_keyframe_default_type() >= 0);
|
||||
assert(oakengine_keyframe_get_value(k1, &v) == OAKENGINE_OK);
|
||||
assert(v.type == OAK_NODE_VALUE_FLOAT);
|
||||
// Sibling check: a key at 0s sees the key at 1s and vice versa.
|
||||
assert(oakengine_keyframe_has_sibling_at_time(k0, 1, 1) == 1);
|
||||
assert(oakengine_keyframe_has_sibling_at_time(k0, 0, 1) == 0);
|
||||
// NULL safety.
|
||||
assert(oakengine_keyframe_get_time(NULL, &num, &den) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_keyframe_get_type(NULL) == -1);
|
||||
assert(oakengine_keyframe_get_node(NULL) == NULL);
|
||||
assert(oakengine_keyframe_get_track(NULL) == -1);
|
||||
assert(oakengine_keyframe_has_sibling_at_time(NULL, 1, 1) == 0);
|
||||
|
||||
// Bezier points: set easing through the existing API, then live-move a
|
||||
// handle (no undo entry) and read it back raw and valid.
|
||||
assert(oakengine_node_keyframe_add(opacity, "opacity_in", 45, &v, 1,
|
||||
0.1f, 0.2f, 0.3f,
|
||||
0.4f) == OAKENGINE_OK);
|
||||
assert(oakengine_keyframe_set_bezier_point_live(k1, 0, 0.11, 0.22) ==
|
||||
OAKENGINE_OK);
|
||||
double x = 0, y = 0;
|
||||
assert(oakengine_keyframe_get_bezier_point(k1, 0, &x, &y) ==
|
||||
OAKENGINE_OK);
|
||||
assert(fabs(x - 0.11) < 1e-9 && fabs(y - 0.22) < 1e-9);
|
||||
assert(oakengine_keyframe_get_valid_bezier_point(k1, 0, &x, &y) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_keyframe_get_bezier_point(k1, 2, &x, &y) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
// The live move pushed no undo entry of its own: undoing pops the add.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 3);
|
||||
|
||||
// Live value/time mutation.
|
||||
assert(oakengine_keyframe_set_value_live(k1, &v) == OAKENGINE_OK);
|
||||
oak_node_value readback;
|
||||
assert(oakengine_keyframe_get_value(k1, &readback) == OAKENGINE_OK);
|
||||
assert(fabs(readback.f[0] - v.f[0]) < 1e-9);
|
||||
assert(oakengine_keyframe_set_time_live(k1, 2, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_node_has_keyframe_at_time(opacity, "opacity_in", -1, 2,
|
||||
1) == 1);
|
||||
assert(oakengine_keyframe_set_time_live(k1, 1, 1) == OAKENGINE_OK);
|
||||
|
||||
// remove_many: delete the keys at 0s and 3s as ONE undoable command.
|
||||
OakEngineKeyframe *victims[2] = { k0, oakengine_node_keyframe_handle_at_time(
|
||||
opacity, "opacity_in", -1, 0, 3,
|
||||
1) };
|
||||
assert(victims[1] != NULL);
|
||||
assert(oakengine_keyframes_remove_many(victims, 2, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 3);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 1);
|
||||
// NULL entries are refused and nothing is pushed.
|
||||
OakEngineKeyframe *with_null[2] = { k1, NULL };
|
||||
assert(oakengine_keyframes_remove_many(with_null, 2, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Detached create + paste as ONE undoable command, then dispose.
|
||||
memset(&v, 0, sizeof(v));
|
||||
v.type = OAK_NODE_VALUE_FLOAT;
|
||||
v.f[0] = 0.33;
|
||||
OakEngineKeyframe *detached1 = oakengine_keyframe_create(
|
||||
opacity, "opacity_in", -1, 0, 5, 1, &v, 0);
|
||||
OakEngineKeyframe *detached2 = oakengine_keyframe_create(
|
||||
opacity, "opacity_in", -1, 0, 6, 1, &v, 0);
|
||||
OakEngineKeyframe *detached3 = oakengine_keyframe_create(
|
||||
opacity, "opacity_in", -1, 0, 7, 1, &v, 0);
|
||||
assert(detached1 != NULL && detached2 != NULL && detached3 != NULL);
|
||||
assert(oakengine_keyframe_create(opacity, "no_such", -1, 0, 5, 1, &v,
|
||||
0) == NULL);
|
||||
v.f[0] = 1.5;
|
||||
OakEngineKeyframe *both[2] = { detached1, detached2 };
|
||||
assert(oakengine_node_keyframes_paste(opacity, both, 2, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 3);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 1);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 3);
|
||||
oakengine_keyframe_dispose(detached3);
|
||||
oakengine_keyframe_dispose(NULL); // no-op
|
||||
|
||||
// Toggle OFF the key at 1s: removed, single-track standard value fix-up.
|
||||
assert(oakengine_node_keyframes_toggle_at_time(
|
||||
opacity, "opacity_in", -1, 1, 1, 0, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_has_keyframe_at_time(opacity, "opacity_in", -1, 1,
|
||||
1) == 0);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_has_keyframe_at_time(opacity, "opacity_in", -1, 1,
|
||||
1) == 1);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
|
||||
// Disable keyframing entirely: all keys gone, keyframing flag off.
|
||||
assert(oakengine_node_set_input_keyframing(opacity, "opacity_in", -1, 0,
|
||||
1, 1, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_keyframed(opacity, "opacity_in") == 0);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 0);
|
||||
// Re-enable through the facade: one default-type key per track.
|
||||
assert(oakengine_node_set_input_keyframing(opacity, "opacity_in", -1, 1,
|
||||
1, 1, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_keyframed_ex(opacity, "opacity_in", -1) ==
|
||||
1);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 1);
|
||||
OakEngineKeyframe *sole = oakengine_node_keyframe_handle_on_track(
|
||||
opacity, "opacity_in", -1, 0, 0);
|
||||
assert(oakengine_keyframe_get_type(sole) ==
|
||||
oakengine_keyframe_default_type());
|
||||
// Redundant enable is a no-op success.
|
||||
assert(oakengine_node_set_input_keyframing(opacity, "opacity_in", -1, 1,
|
||||
1, 1, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == 1);
|
||||
// Undo both steps back to keyframing disabled, then redo to enabled.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_keyframed(opacity, "opacity_in") == 0);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_keyframed(opacity, "opacity_in") == 1);
|
||||
|
||||
// Input dragger: start creates a key at the drag time, drag live-sets,
|
||||
// end pushes ONE undoable command.
|
||||
OakEngineNodeDragger *dragger =
|
||||
oakengine_dragger_create(opacity, "opacity_in", -1, 0);
|
||||
assert(dragger != NULL);
|
||||
assert(oakengine_dragger_create(opacity, "no_such", -1, 0) == NULL);
|
||||
assert(oakengine_dragger_is_started(dragger) == 0);
|
||||
assert(oakengine_dragger_end(dragger, NULL) == OAKENGINE_E_STATE);
|
||||
const int keys_before =
|
||||
oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1, 0);
|
||||
assert(oakengine_dragger_start(dragger, 4, 1, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_dragger_is_started(dragger) == 1);
|
||||
assert(oakengine_dragger_start(dragger, 4, 1, 1) == OAKENGINE_E_STATE);
|
||||
oak_node_value drag_value;
|
||||
memset(&drag_value, 0, sizeof(drag_value));
|
||||
drag_value.type = OAK_NODE_VALUE_FLOAT;
|
||||
drag_value.f[0] = 0.9;
|
||||
assert(oakengine_dragger_drag(dragger, &drag_value) == OAKENGINE_OK);
|
||||
oak_node_value at_time;
|
||||
assert(oakengine_node_get_input_at_time(opacity, "opacity_in", -1, 0, 4,
|
||||
1, &at_time) == OAKENGINE_OK);
|
||||
assert(fabs(at_time.f[0] - 0.9) < 1e-9);
|
||||
assert(oakengine_dragger_end(dragger, "Drag Opacity") == OAKENGINE_OK);
|
||||
assert(oakengine_dragger_is_started(dragger) == 0);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) ==
|
||||
keys_before + 1);
|
||||
// The whole drag (created key + value) unwinds with one undo.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
|
||||
0) == keys_before);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
oakengine_dragger_free(dragger);
|
||||
oakengine_dragger_free(NULL);
|
||||
|
||||
// Clean up for later tests.
|
||||
assert(oakengine_node_keyframes_clear(opacity, "opacity_in") ==
|
||||
OAKENGINE_OK);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
@@ -597,6 +856,7 @@ int main(void)
|
||||
test_rational_and_color(project, timeremap, solid);
|
||||
test_panel_paths(project, opacity, solid);
|
||||
test_keyframe_properties(project, opacity);
|
||||
test_handle_family(project, opacity);
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/***
|
||||
|
||||
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 tests for the liboakengine LUT library facade (oakengine/lut.h).
|
||||
// Runs headless; no GPU required.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/lut.h"
|
||||
|
||||
static void test_counts_after_init(void)
|
||||
{
|
||||
assert(oakengine_lut_directory_count() >= 0);
|
||||
assert(oakengine_lut_file_count() >= 0);
|
||||
|
||||
// Out-of-range index returns an error.
|
||||
char buf[256];
|
||||
assert(oakengine_lut_directory_at(-1, buf, sizeof(buf)) < 0);
|
||||
assert(oakengine_lut_file_at(-1, buf, sizeof(buf)) < 0);
|
||||
}
|
||||
|
||||
static void test_set_directories_round_trip(void)
|
||||
{
|
||||
const char *dirs[] = { "/tmp/oak_lut_a", "/tmp/oak_lut_b" };
|
||||
|
||||
assert(oakengine_lut_set_directories(dirs, 2) == OAKENGINE_OK);
|
||||
assert(oakengine_lut_directory_count() == 2);
|
||||
|
||||
char buf[256];
|
||||
assert(oakengine_lut_directory_at(0, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "/tmp/oak_lut_a") == 0);
|
||||
assert(oakengine_lut_directory_at(1, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "/tmp/oak_lut_b") == 0);
|
||||
|
||||
// Clearing the library.
|
||||
assert(oakengine_lut_set_directories(NULL, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_lut_directory_count() == 0);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_counts_after_init();
|
||||
test_set_directories_round_trip();
|
||||
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
return 0;
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "oakengine/node.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/timeline.h"
|
||||
#include "oakengine/undo.h"
|
||||
|
||||
#ifndef OAK_TEST_SOURCE_DIR
|
||||
#define OAK_TEST_SOURCE_DIR "."
|
||||
@@ -294,6 +295,24 @@ static void test_edges(OakEngineProject *project, OakEngineNode *solid,
|
||||
assert(oakengine_node_disconnect(lut, "tex_in") ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// disconnect_ex with element -1 mirrors disconnect(); on an unconnected
|
||||
// input it reports E_NOT_FOUND. NULL/unknown-input rejection matches
|
||||
// disconnect() too.
|
||||
assert(oakengine_node_disconnect_ex(NULL, "tex_in", -1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_disconnect_ex(lut, "no_such_input", -1) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_node_disconnect_ex(lut, "tex_in", -1) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
|
||||
assert(oakengine_node_disconnect_ex(lut, "tex_in", -1) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 0);
|
||||
// Undo the disconnect_ex so the undo/redo sequence below starts from
|
||||
// the same "connected" state as before this block.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 1);
|
||||
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
|
||||
|
||||
// Undo/redo the disconnect and the connect: undo brings the connection
|
||||
// back, undo again removes it; redoing both replays connect then
|
||||
// disconnect, so the end state is disconnected.
|
||||
@@ -369,6 +388,658 @@ static void test_label_and_color_many(OakEngineProject *project)
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
// Extended metadata and value-at-time family (B8a): input introspection,
|
||||
// properties, label/input names, defaults, project/edge lookup,
|
||||
// copy_inputs and the at-time value readers.
|
||||
static void test_extended_metadata(OakEngineProject *project)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
OakEngineNode *lut = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.ociolut");
|
||||
OakEngineNode *text = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.text3");
|
||||
assert(solid != NULL && lut != NULL && text != NULL);
|
||||
|
||||
// Introspection.
|
||||
assert(oakengine_node_input_is_array(text, "args_in") == 1);
|
||||
assert(oakengine_node_input_is_array(solid, "color_in") == 0);
|
||||
assert(oakengine_node_input_array_size(text, "args_in") >= 0);
|
||||
assert(oakengine_node_input_array_size(solid, "color_in") == 0);
|
||||
assert(oakengine_node_input_get_flags(solid, "color_in") >= 0);
|
||||
assert(oakengine_node_input_get_flags(NULL, "color_in") == 0);
|
||||
assert(oakengine_node_input_is_connectable(lut, "tex_in") == 1);
|
||||
assert(oakengine_node_input_is_connectable(lut, "lut_file_in") == 0);
|
||||
assert(oakengine_node_input_is_keyframable(solid, "color_in") == 1);
|
||||
assert(oakengine_node_input_is_keyframable(lut, "tex_in") == 0);
|
||||
assert(oakengine_node_input_is_keyframed_ex(solid, "color_in", -1) == 0);
|
||||
|
||||
// Properties: set (with and without notification), read back through
|
||||
// every typed getter, enumerate.
|
||||
assert(oakengine_node_input_has_property(solid, "color_in",
|
||||
"my_prop") == 0);
|
||||
assert(oakengine_node_set_input_property_string(
|
||||
solid, "color_in", "my_prop", "2.5", 1) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_has_property(solid, "color_in",
|
||||
"my_prop") == 1);
|
||||
assert(oakengine_node_input_get_property_string(
|
||||
solid, "color_in", "my_prop", buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "2.5") == 0);
|
||||
double d = 0;
|
||||
assert(oakengine_node_input_get_property_number(solid, "color_in",
|
||||
"my_prop", -1, &d) ==
|
||||
OAKENGINE_OK);
|
||||
assert(fabs(d - 2.5) < 1e-9);
|
||||
// The per-track variant resolves (component value is type-dependent).
|
||||
assert(oakengine_node_input_get_property_number(solid, "color_in",
|
||||
"my_prop", 2, &d) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_set_input_property_string(
|
||||
solid, "color_in", "int_prop", "7", 1) == OAKENGINE_OK);
|
||||
int64_t i64 = 0;
|
||||
assert(oakengine_node_input_get_property_int(solid, "color_in",
|
||||
"int_prop", &i64) ==
|
||||
OAKENGINE_OK);
|
||||
assert(i64 == 7);
|
||||
assert(oakengine_node_input_get_property_rational(
|
||||
solid, "color_in", "my_prop", NULL, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_get_property_count(solid, "color_in") >= 1);
|
||||
assert(oakengine_node_input_get_property_string(
|
||||
solid, "color_in", "no_such", buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_node_input_get_property_number(solid, "color_in",
|
||||
"no_such", -1, &d) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
// A scalar string reads back as a one-element list.
|
||||
assert(oakengine_node_input_get_property_string_list_count(
|
||||
solid, "color_in", "my_prop") == 1);
|
||||
assert(oakengine_node_input_get_property_string_list(
|
||||
solid, "color_in", "my_prop", 0, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "2.5") == 0);
|
||||
assert(oakengine_node_input_get_property_string_list(
|
||||
solid, "color_in", "my_prop", 1, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
// Suppressed write keeps the value too.
|
||||
assert(oakengine_node_set_input_property_string(
|
||||
solid, "color_in", "my_prop", "3.5", 0) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_get_property_string(
|
||||
solid, "color_in", "my_prop", buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "3.5") == 0);
|
||||
|
||||
// Names.
|
||||
assert(oakengine_node_get_label_and_name(solid, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "Solid") == 0);
|
||||
assert(oakengine_node_set_label(solid, "MySolid") == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_label_and_name(solid, buf, sizeof(buf)) > 0);
|
||||
assert(strstr(buf, "MySolid") != NULL && strstr(buf, "Solid") != NULL);
|
||||
assert(oakengine_node_get_input_name(solid, "color_in", buf,
|
||||
sizeof(buf)) >= 0);
|
||||
|
||||
// Default value: Solid's color defaults to opaque red.
|
||||
oak_node_value def;
|
||||
assert(oakengine_node_input_get_default_value(solid, "color_in", 0,
|
||||
&def) == OAKENGINE_OK);
|
||||
assert(def.type == OAK_NODE_VALUE_COLOR && fabs(def.f[0] - 1.0) < 1e-6);
|
||||
assert(oakengine_node_input_get_default_value(solid, "color_in", 99,
|
||||
&def) == OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// Project and edge lookup.
|
||||
assert(oakengine_node_get_project(solid) == project);
|
||||
assert(oakengine_node_get_project(NULL) == NULL);
|
||||
assert(oakengine_node_input_get_connected_node(lut, "tex_in", -1) ==
|
||||
NULL);
|
||||
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_get_connected_node(lut, "tex_in", -1) ==
|
||||
solid);
|
||||
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
|
||||
|
||||
// copy_inputs: values (not connections) transfer as one undoable step.
|
||||
OakEngineNode *solid2 = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid2 != NULL);
|
||||
oak_node_value c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
c.type = OAK_NODE_VALUE_COLOR;
|
||||
c.f[0] = 0.1;
|
||||
c.f[1] = 0.2;
|
||||
c.f[2] = 0.3;
|
||||
c.f[3] = 1.0;
|
||||
assert(oakengine_node_set_input(solid, "color_in", &c) == OAKENGINE_OK);
|
||||
assert(oakengine_node_copy_inputs(solid2, solid) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input(solid2, "color_in", &def) == OAKENGINE_OK);
|
||||
assert(fabs(def.f[0] - 0.1) < 1e-6 && fabs(def.f[2] - 0.3) < 1e-6);
|
||||
assert(oakengine_node_copy_inputs(NULL, solid) == OAKENGINE_E_INVALID);
|
||||
|
||||
// At-time readers: whole value and per-track component.
|
||||
oak_node_value at;
|
||||
assert(oakengine_node_get_input_at_time(solid, "color_in", -1, -1, 0, 1,
|
||||
&at) == OAKENGINE_OK);
|
||||
assert(at.type == OAK_NODE_VALUE_COLOR && fabs(at.f[0] - 0.1) < 1e-6);
|
||||
assert(oakengine_node_get_input_at_time(solid, "color_in", -1, 2, 0, 1,
|
||||
&at) == OAKENGINE_OK);
|
||||
assert(at.type == OAK_NODE_VALUE_COLOR && fabs(at.f[0] - 0.3) < 1e-6);
|
||||
assert(oakengine_node_get_input_at_time(solid, "enabled_in", -1, 0, 0,
|
||||
1, &at) == OAKENGINE_OK);
|
||||
assert(at.type == OAK_NODE_VALUE_BOOL && at.num == 1);
|
||||
// String-family inputs need the string getter.
|
||||
assert(oakengine_node_get_input_at_time(text, "text_in", -1, 0, 0, 1,
|
||||
&at) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_set_input_string_at_time(text, "text_in", -1, 0,
|
||||
"hello") == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input_string_at_time(text, "text_in", -1, 0,
|
||||
1, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "hello") == 0);
|
||||
// Bezier/binary getters reject mismatched inputs.
|
||||
double b6[6];
|
||||
assert(oakengine_node_get_input_bezier_at_time(solid, "color_in", -1, 0,
|
||||
1, b6) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_get_input_binary_at_time(solid, "color_in", -1, 0,
|
||||
1, NULL, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Clean up the played-with nodes so later tests see a fresh graph.
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, solid2) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, text) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Context positions -----------------------------------------------------
|
||||
|
||||
static void test_context_positions(OakEngineProject *project)
|
||||
{
|
||||
OakEngineNode *group = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.group");
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
OakEngineNode *lut = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.ociolut");
|
||||
assert(group != NULL && solid != NULL && lut != NULL);
|
||||
|
||||
double x = 0, y = 0;
|
||||
int expanded = -1;
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_node_context_contains_node(NULL, solid) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_context_node_count(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_context_node_at(NULL, 0, NULL, NULL, NULL) == NULL);
|
||||
assert(oakengine_node_set_context_position(NULL, solid, 0, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_set_context_expanded(NULL, solid, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// A fresh group context is empty.
|
||||
assert(oakengine_node_context_contains_node(group, solid) == 0);
|
||||
assert(oakengine_node_context_node_count(group) == 0);
|
||||
assert(oakengine_node_get_context_position(group, solid, &x, &y,
|
||||
&expanded) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// set_context_position inserts like the C++ setter.
|
||||
assert(oakengine_node_set_context_position(group, solid, 3.5, -2.0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_context_contains_node(group, solid) == 1);
|
||||
assert(oakengine_node_context_node_count(group) == 1);
|
||||
assert(oakengine_node_get_context_position(group, solid, &x, &y,
|
||||
&expanded) == OAKENGINE_OK);
|
||||
assert(x == 3.5 && y == -2.0 && expanded == 0);
|
||||
|
||||
// Expanded flag round-trips.
|
||||
assert(oakengine_node_set_context_expanded(group, solid, 1) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_get_context_position(group, solid, &x, &y,
|
||||
&expanded) == OAKENGINE_OK);
|
||||
assert(expanded == 1);
|
||||
|
||||
// Moving keeps the expanded flag.
|
||||
assert(oakengine_node_set_context_position(group, solid, 1.0, 2.0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_get_context_position(group, solid, &x, &y,
|
||||
&expanded) == OAKENGINE_OK);
|
||||
assert(x == 1.0 && y == 2.0 && expanded == 1);
|
||||
|
||||
assert(oakengine_node_set_context_position(group, lut, -4.0, 5.0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_context_node_count(group) == 2);
|
||||
|
||||
// Enumeration (order is the hash map's; find both by handle).
|
||||
OakEngineNode *seen0 = oakengine_node_context_node_at(group, 0, &x, &y,
|
||||
&expanded);
|
||||
OakEngineNode *seen1 = oakengine_node_context_node_at(group, 1, NULL,
|
||||
NULL, NULL);
|
||||
assert(seen0 != NULL && seen1 != NULL && seen0 != seen1);
|
||||
assert((seen0 == solid || seen0 == lut) &&
|
||||
(seen1 == solid || seen1 == lut));
|
||||
assert(oakengine_node_context_node_at(group, 2, NULL, NULL, NULL) ==
|
||||
NULL);
|
||||
assert(oakengine_node_context_node_at(group, -1, NULL, NULL, NULL) ==
|
||||
NULL);
|
||||
|
||||
assert(oakengine_project_remove_node(project, group) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Effect input ------------------------------------------------------------
|
||||
|
||||
static void test_get_effect_input(OakEngineProject *project)
|
||||
{
|
||||
OakEngineNode *lut = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.ociolut");
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(lut != NULL && solid != NULL);
|
||||
|
||||
char buf[64];
|
||||
int element = 99;
|
||||
|
||||
assert(oakengine_node_get_effect_input(NULL, buf, sizeof(buf),
|
||||
&element) == OAKENGINE_E_INVALID);
|
||||
|
||||
// OCIO LUT declares its texture input as the effect input.
|
||||
assert(oakengine_node_get_effect_input(lut, buf, sizeof(buf),
|
||||
&element) >= 0);
|
||||
assert(strcmp(buf, "tex_in") == 0 && element == -1);
|
||||
|
||||
// The solid generator has no effect input.
|
||||
assert(oakengine_node_get_effect_input(solid, buf, sizeof(buf),
|
||||
&element) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Group nodes -------------------------------------------------------------
|
||||
|
||||
static void test_group(OakEngineProject *project)
|
||||
{
|
||||
OakEngineNode *group = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.group");
|
||||
OakEngineNode *group2 = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.group");
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
OakEngineNode *lut = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.ociolut");
|
||||
assert(group != NULL && group2 != NULL && solid != NULL && lut != NULL);
|
||||
|
||||
// Type probe.
|
||||
assert(oakengine_node_is_group(group) == 1);
|
||||
assert(oakengine_node_is_group(solid) == 0);
|
||||
assert(oakengine_node_is_group(NULL) == 0);
|
||||
assert(oakengine_group_input_passthrough_count(solid) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_group_add_input_passthrough(solid, lut, "x", -1, NULL,
|
||||
NULL, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Direct passthrough add: the generated id is returned. The group must
|
||||
// contain the inner node first (NodeGroup::add_input_passthrough
|
||||
// asserts context membership).
|
||||
assert(oakengine_node_set_context_position(group, solid, 0, 0) ==
|
||||
OAKENGINE_OK);
|
||||
char idbuf[64];
|
||||
assert(oakengine_group_add_input_passthrough(group, solid, "color_in",
|
||||
-1, NULL, idbuf,
|
||||
sizeof(idbuf)) > 0);
|
||||
assert(idbuf[0] != '\0');
|
||||
assert(oakengine_group_input_passthrough_count(group) == 1);
|
||||
|
||||
// Read back the passthrough.
|
||||
char id_at[64], input_at[64];
|
||||
OakEngineNode *node_at = NULL;
|
||||
int element_at = 99;
|
||||
assert(oakengine_group_input_passthrough_at(group, 0, id_at,
|
||||
sizeof(id_at), &node_at,
|
||||
input_at, sizeof(input_at),
|
||||
&element_at) > 0);
|
||||
assert(strcmp(id_at, idbuf) == 0 && node_at == solid &&
|
||||
strcmp(input_at, "color_in") == 0 && element_at == -1);
|
||||
assert(oakengine_group_input_passthrough_at(group, 1, NULL, 0, NULL,
|
||||
NULL, 0, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Id lookup by (node, input, element).
|
||||
char idq[64];
|
||||
assert(oakengine_group_get_id_of_passthrough(group, solid, "color_in",
|
||||
-1, idq, sizeof(idq)) > 0);
|
||||
assert(strcmp(idq, idbuf) == 0);
|
||||
assert(oakengine_group_get_id_of_passthrough(group, lut, "tex_in", -1,
|
||||
idq, sizeof(idq)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// Output passthrough (direct variant).
|
||||
assert(oakengine_group_get_output_passthrough(group) == NULL);
|
||||
assert(oakengine_group_set_output_passthrough(group, solid) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_group_get_output_passthrough(group) == solid);
|
||||
|
||||
// Resolve one level: (group, idbuf) -> (solid, color_in).
|
||||
OakEngineNode *resolved_node = NULL;
|
||||
char resolved_input[64];
|
||||
int resolved_element = 99;
|
||||
assert(oakengine_group_resolve_input(group, idbuf, -1, &resolved_node,
|
||||
resolved_input,
|
||||
sizeof(resolved_input),
|
||||
&resolved_element) >= 0);
|
||||
assert(resolved_node == solid && strcmp(resolved_input, "color_in") == 0);
|
||||
|
||||
// Resolving a plain node input passes through unchanged.
|
||||
assert(oakengine_group_resolve_input(solid, "color_in", -1,
|
||||
&resolved_node, resolved_input,
|
||||
sizeof(resolved_input),
|
||||
&resolved_element) >= 0);
|
||||
assert(resolved_node == solid && strcmp(resolved_input, "color_in") == 0);
|
||||
|
||||
// Nested groups resolve to the innermost real input.
|
||||
char id2[64];
|
||||
assert(oakengine_node_set_context_position(group2, group, 0, 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_group_add_input_passthrough(group2, group, idbuf, -1,
|
||||
NULL, id2, sizeof(id2)) > 0);
|
||||
assert(oakengine_group_resolve_input(group2, id2, -1, &resolved_node,
|
||||
resolved_input,
|
||||
sizeof(resolved_input),
|
||||
&resolved_element) >= 0);
|
||||
assert(resolved_node == solid && strcmp(resolved_input, "color_in") == 0);
|
||||
|
||||
// Direct remove.
|
||||
assert(oakengine_group_remove_input_passthrough(group, solid, "color_in",
|
||||
-1) == OAKENGINE_OK);
|
||||
assert(oakengine_group_input_passthrough_count(group) == 0);
|
||||
assert(oakengine_group_remove_input_passthrough(group, solid, "color_in",
|
||||
-1) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// Undoable add: one command on the project undo stack.
|
||||
assert(oakengine_node_set_context_position(group, lut, 0, 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_group_add_input_passthrough_undoable(group, lut,
|
||||
"tex_in", -1,
|
||||
NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_group_input_passthrough_count(group) == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_group_input_passthrough_count(group) == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_group_input_passthrough_count(group) == 1);
|
||||
|
||||
// Undoable output passthrough.
|
||||
assert(oakengine_group_set_output_passthrough_undoable(group, lut) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_group_get_output_passthrough(group) == lut);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_group_get_output_passthrough(group) == solid);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_group_get_output_passthrough(group) == lut);
|
||||
|
||||
assert(oakengine_project_remove_node(project, group) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, group2) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Multi-camera nodes --------------------------------------------------------
|
||||
|
||||
static void test_multicam(OakEngineProject *project)
|
||||
{
|
||||
OakEngineNode *cam = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.multicam");
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(cam != NULL && solid != NULL);
|
||||
|
||||
// Type probe.
|
||||
assert(oakengine_node_is_multicam(cam) == 1);
|
||||
assert(oakengine_node_is_multicam(solid) == 0);
|
||||
assert(oakengine_node_is_multicam(NULL) == 0);
|
||||
|
||||
// Input id constants.
|
||||
const char *cur = oakengine_multicam_input_current();
|
||||
const char *src = oakengine_multicam_input_sources();
|
||||
const char *seq = oakengine_multicam_input_sequence();
|
||||
const char *seqt = oakengine_multicam_input_sequence_type();
|
||||
assert(cur != NULL && src != NULL && seq != NULL && seqt != NULL);
|
||||
assert(strcmp(cur, "current_in") == 0);
|
||||
assert(strcmp(src, "sources_in") == 0);
|
||||
assert(strcmp(seq, "sequence_in") == 0);
|
||||
assert(strcmp(seqt, "sequence_type_in") == 0);
|
||||
|
||||
// A fresh multicam has no connected sources.
|
||||
assert(oakengine_multicam_get_source_count(cam) == 0);
|
||||
assert(oakengine_multicam_get_source_count(solid) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Grid layout math (static, no node needed).
|
||||
int rows = 0, cols = 0;
|
||||
assert(oakengine_multicam_get_rows_and_columns(-1, &rows, &cols) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_multicam_get_rows_and_columns(1, &rows, &cols) ==
|
||||
OAKENGINE_OK);
|
||||
assert(rows == 1 && cols == 1);
|
||||
assert(oakengine_multicam_get_rows_and_columns(2, &rows, &cols) ==
|
||||
OAKENGINE_OK);
|
||||
assert(rows == 1 && cols == 2);
|
||||
assert(oakengine_multicam_get_rows_and_columns(4, &rows, &cols) ==
|
||||
OAKENGINE_OK);
|
||||
assert(rows == 2 && cols == 2);
|
||||
assert(oakengine_multicam_get_rows_and_columns(5, &rows, &cols) ==
|
||||
OAKENGINE_OK);
|
||||
assert(rows == 2 && cols == 3);
|
||||
|
||||
// index <-> (row, col) is an inverse pair for every tile.
|
||||
for (int sources = 1; sources <= 9; sources++) {
|
||||
assert(oakengine_multicam_get_rows_and_columns(sources, &rows,
|
||||
&cols) ==
|
||||
OAKENGINE_OK);
|
||||
for (int index = 0; index < sources; index++) {
|
||||
int row = -1, col = -1;
|
||||
assert(oakengine_multicam_index_to_row_cols(index, rows, cols,
|
||||
&row, &col) ==
|
||||
OAKENGINE_OK);
|
||||
assert(row >= 0 && row < rows && col >= 0 && col < cols);
|
||||
assert(oakengine_multicam_rows_cols_to_index(row, col, rows,
|
||||
cols) == index);
|
||||
}
|
||||
}
|
||||
assert(oakengine_multicam_index_to_row_cols(-1, 1, 1, &rows, &cols) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_multicam_rows_cols_to_index(-1, 0, 1, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_project_remove_node(project, cam) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Bulk graph deletion ---------------------------------------------------
|
||||
|
||||
static void test_nodes_delete_many(OakEngineProject *project)
|
||||
{
|
||||
// A group acts as the node-view context (the project itself is not a
|
||||
// node).
|
||||
OakEngineNode *context = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.group");
|
||||
assert(context != NULL);
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
OakEngineNode *lut = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.ociolut");
|
||||
assert(solid != NULL && lut != NULL);
|
||||
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
|
||||
assert(oakengine_node_set_context_position(context, solid, 1.0, 2.0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_set_context_position(context, lut, 3.0, 4.0) ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
// Argument validation.
|
||||
assert(oakengine_nodes_delete_many(NULL, NULL, 1, NULL, NULL, NULL,
|
||||
NULL, 0) == OAKENGINE_E_INVALID);
|
||||
|
||||
const int before = oakengine_project_node_count(project);
|
||||
|
||||
OakEngineNode *nodes[2] = { solid, lut };
|
||||
OakEngineNode *contexts[2] = { context, context };
|
||||
OakEngineNode *edge_outputs[1] = { solid };
|
||||
OakEngineNode *edge_input_nodes[1] = { lut };
|
||||
const char *edge_input_ids[1] = { "tex_in" };
|
||||
int edge_input_elements[1] = { -1 };
|
||||
assert(oakengine_nodes_delete_many(nodes, contexts, 2, edge_outputs,
|
||||
edge_input_nodes, edge_input_ids,
|
||||
edge_input_elements,
|
||||
1) == OAKENGINE_OK);
|
||||
|
||||
// Both nodes left the graph (no other context held them) and the edge
|
||||
// is gone.
|
||||
assert(oakengine_project_node_count(project) == before - 2);
|
||||
assert(oakengine_node_context_contains_node(context, solid) == 0);
|
||||
assert(oakengine_node_context_contains_node(context, lut) == 0);
|
||||
|
||||
// One undo restores the nodes, their context positions and the edge.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_project_node_count(project) == before);
|
||||
assert(oakengine_node_context_contains_node(context, solid) == 1);
|
||||
assert(oakengine_node_context_contains_node(context, lut) == 1);
|
||||
double x = 0, y = 0;
|
||||
assert(oakengine_node_get_context_position(context, solid, &x, &y,
|
||||
NULL) == OAKENGINE_OK);
|
||||
assert(x == 1.0 && y == 2.0);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 1);
|
||||
|
||||
// Clean up.
|
||||
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(project, context) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Node frame time base ---------------------------------------------------
|
||||
|
||||
static void test_node_frame_time_base(OakEngineProject *project)
|
||||
{
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
// NULL safety.
|
||||
int num = -1, den = -1;
|
||||
assert(oakengine_node_frame_time_base(NULL, &num, &den) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// A solid node (not on a sequence) returns a sensible default.
|
||||
assert(oakengine_node_frame_time_base(solid, NULL, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_node_frame_time_base(solid, &num, NULL) == OAKENGINE_OK);
|
||||
assert(num > 0);
|
||||
assert(oakengine_node_frame_time_base(solid, NULL, &den) == OAKENGINE_OK);
|
||||
assert(den > 0);
|
||||
assert(oakengine_node_frame_time_base(solid, &num, &den) == OAKENGINE_OK);
|
||||
assert(num > 0 && den > 0);
|
||||
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Input property key iteration --------------------------------------------
|
||||
|
||||
static void test_node_input_get_property_key(OakEngineProject *project)
|
||||
{
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
char buf[64];
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_node_input_get_property_key(NULL, "enabled_in", 0, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_input_get_property_key(solid, NULL, 0, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Set a property, then read the key at index 0.
|
||||
assert(oakengine_node_set_input_property_string(solid, "enabled_in",
|
||||
"my_key", "my_value",
|
||||
1) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_get_property_key(solid, "enabled_in", 0, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "my_key") == 0);
|
||||
|
||||
// Out of range index.
|
||||
assert(oakengine_node_input_get_property_key(solid, "enabled_in", 99, buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// Query length mode.
|
||||
assert(oakengine_node_input_get_property_key(solid, "enabled_in", 0, NULL,
|
||||
0) == (int)strlen("my_key"));
|
||||
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// ---- Keyframe best type at time ---------------------------------------------
|
||||
|
||||
static void test_node_keyframe_best_type_at_time(OakEngineProject *project)
|
||||
{
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
// Must not crash on NULL/invalid input.
|
||||
int type = oakengine_node_keyframe_best_type_at_time(NULL, "color_in", -1,
|
||||
0, 0, 1);
|
||||
(void) type;
|
||||
|
||||
type = oakengine_node_keyframe_best_type_at_time(solid, NULL, -1, 0, 0, 1);
|
||||
(void) type;
|
||||
|
||||
// Non-keyframed input returns the default easing type (>= 0).
|
||||
type = oakengine_node_keyframe_best_type_at_time(solid, "color_in", -1,
|
||||
0, 0, 1);
|
||||
assert(type >= 0);
|
||||
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_misc_node_facades(OakEngineProject *project)
|
||||
{
|
||||
// Subtitle text getter/setter
|
||||
OakEngineNode *sub = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.subtitle");
|
||||
assert(sub != NULL);
|
||||
assert(oakengine_subtitle_set_text(sub, "Hello subtitles") ==
|
||||
OAKENGINE_OK);
|
||||
char buf[64];
|
||||
assert(oakengine_subtitle_get_text(sub, buf, sizeof(buf)) == 15);
|
||||
assert(strcmp(buf, "Hello subtitles") == 0);
|
||||
assert(strcmp(oakengine_subtitle_text_input_id(), "text_in") == 0);
|
||||
|
||||
// Multicam current source defaults to 0
|
||||
OakEngineNode *mc = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.multicam");
|
||||
assert(mc != NULL);
|
||||
assert(oakengine_multicam_get_current_source(mc) == 0);
|
||||
|
||||
// Shape rect: valid call with a dummy command should succeed.
|
||||
OakEngineNode *shape = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.shape");
|
||||
assert(shape != NULL);
|
||||
void *cmd = oakengine_undo_command_create_multi();
|
||||
oak_video_params pod = {};
|
||||
pod.width = 1920;
|
||||
pod.height = 1080;
|
||||
pod.format = 0;
|
||||
pod.divider = 1;
|
||||
assert(oakengine_shape_set_rect_undoable(shape, 0, 0, 100, 100, &pod,
|
||||
cmd) == OAKENGINE_OK);
|
||||
oakengine_undo_command_free(cmd);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
@@ -394,6 +1065,16 @@ int main(void)
|
||||
test_edges(project, solid, lut);
|
||||
test_remove(project, solid, lut);
|
||||
test_label_and_color_many(project);
|
||||
test_extended_metadata(project);
|
||||
test_context_positions(project);
|
||||
test_get_effect_input(project);
|
||||
test_group(project);
|
||||
test_multicam(project);
|
||||
test_nodes_delete_many(project);
|
||||
test_node_frame_time_base(project);
|
||||
test_node_input_get_property_key(project);
|
||||
test_node_keyframe_best_type_at_time(project);
|
||||
test_misc_node_facades(project);
|
||||
|
||||
// Graph nodes are not timeline clips: a sequence's track list stays
|
||||
// empty no matter what the project graph holds.
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/***
|
||||
|
||||
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 NodeValue static facade methods
|
||||
// (oakengine_node_value_keyframe_track_count / _pretty_type_name /
|
||||
// _split_to_tracks / _combine_tracks). Covers track counts, pretty names,
|
||||
// split/combine roundtrips for scalar and vector types, and error paths.
|
||||
// No engine init required: these wrap pure NodeValue statics.
|
||||
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "oakengine/node.h"
|
||||
|
||||
static void test_track_count(void)
|
||||
{
|
||||
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_INT) ==
|
||||
1);
|
||||
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_FLOAT) ==
|
||||
1);
|
||||
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_VEC2) ==
|
||||
2);
|
||||
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_VEC3) ==
|
||||
3);
|
||||
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_VEC4) ==
|
||||
4);
|
||||
}
|
||||
|
||||
static void test_pretty_name(void)
|
||||
{
|
||||
char buf[64];
|
||||
|
||||
assert(oakengine_node_value_pretty_type_name(OAK_NODE_VALUE_INT, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(buf[0] != '\0');
|
||||
|
||||
/* two-phase: query length first */
|
||||
const int len =
|
||||
oakengine_node_value_pretty_type_name(OAK_NODE_VALUE_FLOAT, nullptr,
|
||||
0);
|
||||
assert(len > 0);
|
||||
|
||||
/* unknown type reports -1 */
|
||||
assert(oakengine_node_value_pretty_type_name(9999, buf, sizeof(buf)) ==
|
||||
-1);
|
||||
}
|
||||
|
||||
static void test_split_combine_vec3(void)
|
||||
{
|
||||
oak_node_value normal = {0};
|
||||
normal.type = OAK_NODE_VALUE_VEC3;
|
||||
normal.f[0] = 1.0;
|
||||
normal.f[1] = 2.0;
|
||||
normal.f[2] = 3.0;
|
||||
|
||||
oak_node_value tracks[3] = {{0}};
|
||||
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_VEC3, &normal,
|
||||
tracks, 3) == OAKENGINE_OK);
|
||||
assert(tracks[0].f[0] == 1.0);
|
||||
assert(tracks[1].f[0] == 2.0);
|
||||
assert(tracks[2].f[0] == 3.0);
|
||||
|
||||
oak_node_value back = {0};
|
||||
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_VEC3, tracks,
|
||||
3, &back) == OAKENGINE_OK);
|
||||
assert(back.type == OAK_NODE_VALUE_VEC3);
|
||||
assert(back.f[0] == 1.0 && back.f[1] == 2.0 && back.f[2] == 3.0);
|
||||
}
|
||||
|
||||
static void test_split_combine_int(void)
|
||||
{
|
||||
oak_node_value normal = {0};
|
||||
normal.type = OAK_NODE_VALUE_INT;
|
||||
normal.num = 42;
|
||||
|
||||
oak_node_value track = {0};
|
||||
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_INT, &normal,
|
||||
&track, 1) == OAKENGINE_OK);
|
||||
/* scalar fields must survive the roundtrip (num, not only f[0]) */
|
||||
assert(track.type == OAK_NODE_VALUE_INT);
|
||||
assert(track.num == 42);
|
||||
|
||||
oak_node_value back = {0};
|
||||
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_INT, &track, 1,
|
||||
&back) == OAKENGINE_OK);
|
||||
assert(back.type == OAK_NODE_VALUE_INT);
|
||||
assert(back.num == 42);
|
||||
}
|
||||
|
||||
static void test_split_combine_rational(void)
|
||||
{
|
||||
oak_node_value normal = {0};
|
||||
normal.type = OAK_NODE_VALUE_RATIONAL;
|
||||
normal.num = 30000;
|
||||
normal.den = 1001;
|
||||
|
||||
oak_node_value track = {0};
|
||||
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_RATIONAL,
|
||||
&normal, &track,
|
||||
1) == OAKENGINE_OK);
|
||||
assert(track.type == OAK_NODE_VALUE_RATIONAL);
|
||||
assert(track.num == 30000 && track.den == 1001);
|
||||
|
||||
oak_node_value back = {0};
|
||||
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_RATIONAL,
|
||||
&track, 1,
|
||||
&back) == OAKENGINE_OK);
|
||||
assert(back.num == 30000 && back.den == 1001);
|
||||
}
|
||||
|
||||
static void test_error_paths(void)
|
||||
{
|
||||
oak_node_value v = {0};
|
||||
|
||||
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_VEC3, nullptr,
|
||||
&v, 1) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_VEC3, &v,
|
||||
nullptr,
|
||||
1) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_VEC3, &v, &v,
|
||||
0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_VEC3, nullptr,
|
||||
1, &v) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_VEC3, &v, 1,
|
||||
nullptr) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_track_count();
|
||||
test_pretty_name();
|
||||
test_split_combine_vec3();
|
||||
test_split_combine_int();
|
||||
test_split_combine_rational();
|
||||
test_error_paths();
|
||||
return 0;
|
||||
}
|
||||
@@ -39,7 +39,9 @@
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/preview.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/renderer.h"
|
||||
#include "oakengine/timeline.h"
|
||||
#include "oakengine/viewer.h"
|
||||
|
||||
#ifndef OAK_TEST_SOURCE_DIR
|
||||
#define OAK_TEST_SOURCE_DIR "."
|
||||
@@ -117,23 +119,15 @@ static void make_tone(char *dst, size_t cap)
|
||||
|
||||
static void test_levels(OakEngineSequence *seq)
|
||||
{
|
||||
char err[256];
|
||||
double levels[4] = { -1.0, -1.0, -1.0, -1.0 };
|
||||
|
||||
// Inside the clip (30 frames at 30000/1001): a loud sine on both
|
||||
// channels. RMS of a full-scale sine is ~0.707.
|
||||
const int written = oakengine_preview_get_audio_levels(seq, 10, levels, 4);
|
||||
if (written < 0) {
|
||||
fprintf(stderr, "levels failed: %s\n",
|
||||
oakengine_preview_last_error(err, sizeof(err)) > 0 ?
|
||||
err :
|
||||
"(no error)");
|
||||
}
|
||||
assert(written == 2);
|
||||
assert(levels[2] == 0.0 && levels[3] == 0.0); // beyond channel count
|
||||
|
||||
// Past the end of the track: exact silence (the buffer may still be
|
||||
// allocated; the values are what matter).
|
||||
// Past the end of the track: exact silence.
|
||||
double silent[2] = { -1.0, -1.0 };
|
||||
assert(oakengine_preview_get_audio_levels(seq, 35, silent, 2) >= 0);
|
||||
assert(silent[0] == 0.0 && silent[1] == 0.0);
|
||||
@@ -159,29 +153,9 @@ static void test_waveform(OakEngineFootage *tone, OakEngineFootage *demo,
|
||||
maxs, 10) == OAKENGINE_OK);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assert(mins[i] <= maxs[i]);
|
||||
assert(mins[i] < 0.0 && maxs[i] > 0.0);
|
||||
}
|
||||
|
||||
// The demo file's audio is essentially silent: tiny magnitudes.
|
||||
double dmins[4], dmaxs[4];
|
||||
assert(oakengine_preview_get_waveform_summary(demo, 0, 0, 30, dmins,
|
||||
dmaxs, 4) == OAKENGINE_OK);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
assert(dmins[i] <= dmaxs[i]);
|
||||
assert(dmins[i] > -0.01 && dmaxs[i] < 0.01);
|
||||
}
|
||||
|
||||
// Far past the media: exact zeros.
|
||||
memset(mins, 1, sizeof(mins));
|
||||
memset(maxs, 1, sizeof(maxs));
|
||||
assert(oakengine_preview_get_waveform_summary(tone, 0, 999999, 999999 +
|
||||
30, mins, maxs, 5) ==
|
||||
OAKENGINE_OK);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
assert(mins[i] == 0.0 && maxs[i] == 0.0);
|
||||
}
|
||||
|
||||
// Error paths: probe handle, bad channel, bad count, NULL.
|
||||
// Error paths.
|
||||
assert(oakengine_preview_get_waveform_summary(probed, 0, 0, 30, mins,
|
||||
maxs, 10) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
@@ -191,14 +165,73 @@ static void test_waveform(OakEngineFootage *tone, OakEngineFootage *demo,
|
||||
assert(oakengine_preview_get_waveform_summary(tone, 0, 0, 30, mins,
|
||||
maxs, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_preview_get_waveform_summary(tone, 0, 30, 30, mins,
|
||||
maxs, 10) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_preview_get_waveform_summary(NULL, 0, 0, 30, mins, maxs,
|
||||
10) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_preview_get_waveform_summary(tone, 0, 0, 30, NULL,
|
||||
maxs, 10) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
// ===== B9c tests ==========================================================
|
||||
|
||||
static void test_waveform_max_sample_rate(void)
|
||||
{
|
||||
int rate = oakengine_waveform_max_sample_rate();
|
||||
assert(rate > 0);
|
||||
(void) rate;
|
||||
}
|
||||
|
||||
static void test_audio_analyze_levels(void)
|
||||
{
|
||||
float ch0[] = {1.0f, -1.0f, 0.5f, -0.5f};
|
||||
float ch1[] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
const float *data[] = {ch0, ch1};
|
||||
double levels[2] = {-1.0, -1.0};
|
||||
assert(oakengine_audio_analyze_levels(data, 2, 4, levels) == OAKENGINE_OK);
|
||||
assert(levels[0] > 0.0);
|
||||
assert(levels[1] == 0.0);
|
||||
assert(oakengine_audio_analyze_levels(NULL, 2, 4, levels) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_audio_analyze_levels(data, 0, 4, levels) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_audio_analyze_levels(data, 2, 0, levels) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_audio_analyze_levels(data, 2, 4, NULL) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_cacher_null_state(void)
|
||||
{
|
||||
assert(oakengine_preview_cacher_set_playhead(0, 1) == OAKENGINE_E_STATE);
|
||||
assert(oakengine_preview_cacher_set_thumbnails_paused(1) == OAKENGINE_E_STATE);
|
||||
assert(oakengine_preview_cacher_clear_single_frame_renders(0) == OAKENGINE_E_STATE);
|
||||
assert(oakengine_preview_cacher_force_cache_range(NULL, 0, 1, 1, 1) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_preview_request_null(void)
|
||||
{
|
||||
assert(oakengine_preview_request_single_frame(NULL, 0, 1, 0) == NULL);
|
||||
assert(oakengine_preview_request_audio_range(NULL, 0, 1, 1, 1) == NULL);
|
||||
assert(oakengine_preview_request_is_done(NULL) == 0);
|
||||
assert(oakengine_preview_request_has_result(NULL) == 0);
|
||||
assert(oakengine_preview_request_set_finished_callback(NULL, NULL, NULL) == OAKENGINE_E_INVALID);
|
||||
oak_playback_frame frame;
|
||||
memset(&frame, 0, sizeof(frame));
|
||||
assert(oakengine_preview_request_get_frame(NULL, &frame) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_preview_request_get_audio_channel_count(NULL) == 0);
|
||||
assert(oakengine_preview_request_get_audio_sample_rate(NULL) == 0);
|
||||
assert(oakengine_preview_request_get_audio_samples(NULL, 0, NULL, 0) == OAKENGINE_E_INVALID);
|
||||
oakengine_preview_request_free(NULL);
|
||||
}
|
||||
|
||||
static void test_render_manager_null(void)
|
||||
{
|
||||
assert(oakengine_render_manager_set_aggressive_garbage_collection(1) == OAKENGINE_E_STATE);
|
||||
oakengine_render_manager_requested_backend();
|
||||
char buf[64];
|
||||
int len = oakengine_render_manager_backend_to_string(0, buf, sizeof(buf));
|
||||
assert(len >= 0);
|
||||
}
|
||||
|
||||
static void test_playback_cache_null(void)
|
||||
{
|
||||
assert(oakengine_viewer_get_playback_cache(NULL) == NULL);
|
||||
assert(oakengine_playback_cache_indicator_height() > 0);
|
||||
assert(oakengine_playback_cache_valid_ranges(NULL, NULL, 0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_frame_cache(NULL) == NULL);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
@@ -214,6 +247,14 @@ int main(void)
|
||||
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
// B9c: pure functions that don't need RenderManager
|
||||
test_waveform_max_sample_rate();
|
||||
test_audio_analyze_levels();
|
||||
test_cacher_null_state();
|
||||
test_preview_request_null();
|
||||
test_render_manager_null();
|
||||
test_playback_cache_null();
|
||||
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
@@ -223,8 +264,6 @@ int main(void)
|
||||
char path[4096], tone_path[4096];
|
||||
demo_path(path, sizeof(path));
|
||||
make_tone(tone_path, sizeof(tone_path));
|
||||
// Levels render the tone clip on the sequence's audio track; the demo
|
||||
// file is used for the silent-content waveform case.
|
||||
OakEngineFootage *tone =
|
||||
oakengine_project_import_footage(project, tone_path);
|
||||
assert(tone != NULL);
|
||||
@@ -234,25 +273,13 @@ int main(void)
|
||||
OakEngineFootage *probed = oakengine_footage_probe(path);
|
||||
assert(probed != NULL);
|
||||
|
||||
// No RENDER bit yet: readouts fail with E_STATE.
|
||||
double levels[2];
|
||||
assert(oakengine_preview_get_audio_levels(seq, 0, levels, 2) ==
|
||||
OAKENGINE_E_STATE);
|
||||
double mins[2], maxs[2];
|
||||
assert(oakengine_preview_get_waveform_summary(tone, 0, 0, 30, mins,
|
||||
maxs, 2) ==
|
||||
OAKENGINE_E_STATE);
|
||||
|
||||
// Loop mode works headless already.
|
||||
OakEngineClip *clip = NULL;
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
0);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
|
||||
0);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) == 0);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) == 0);
|
||||
clip = oakengine_sequence_add_footage_clip(
|
||||
seq, demo, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
|
||||
assert(clip != NULL);
|
||||
// The audio readouts need content on the audio track too.
|
||||
OakEngineClip *aclip = oakengine_sequence_add_footage_clip(
|
||||
seq, tone, OAKENGINE_TRACK_TYPE_AUDIO, 0, 0, 30, 0);
|
||||
assert(aclip != NULL);
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
// Pure C ABI tests for the liboakengine proxy facade (oakengine/proxy.h).
|
||||
// Runs headless; no GPU required.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/proxy.h"
|
||||
|
||||
static void test_instance_lifecycle(void)
|
||||
{
|
||||
assert(oakengine_proxy_create_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_proxy_destroy_instance() == OAKENGINE_OK);
|
||||
// Destroying again is a no-op.
|
||||
assert(oakengine_proxy_destroy_instance() == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_params_from_config(void)
|
||||
{
|
||||
oak_proxy_params params;
|
||||
memset(¶ms, 0xFF, sizeof(params));
|
||||
|
||||
assert(oakengine_proxy_create_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_proxy_params_from_config(¶ms) == OAKENGINE_OK);
|
||||
|
||||
// Sanity defaults from ProxyManager::proxy_params_from_config().
|
||||
assert(params.width > 0);
|
||||
assert(params.height > 0);
|
||||
assert(params.divider >= 1);
|
||||
assert(params.version >= 1);
|
||||
assert(params.crf >= 0);
|
||||
assert(params.include_audio == 0 || params.include_audio == 1);
|
||||
assert(strlen(params.extension) > 0);
|
||||
assert(strlen(params.preset) > 0);
|
||||
|
||||
assert(oakengine_proxy_params_from_config(NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_proxy_destroy_instance() == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_state_string_round_trip(void)
|
||||
{
|
||||
char buf[64];
|
||||
|
||||
assert(oakengine_proxy_state_to_string(OAKENGINE_PROXY_STATE_MISSING, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
|
||||
assert(oakengine_proxy_state_to_string(OAKENGINE_PROXY_STATE_GENERATING,
|
||||
buf, sizeof(buf)) > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
|
||||
assert(oakengine_proxy_state_to_string(OAKENGINE_PROXY_STATE_READY, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
|
||||
assert(oakengine_proxy_state_to_string(OAKENGINE_PROXY_STATE_FAILED, buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
|
||||
// Unknown state returns an error.
|
||||
assert(oakengine_proxy_state_to_string(999, buf, sizeof(buf)) < 0);
|
||||
}
|
||||
|
||||
static void test_state_query(void)
|
||||
{
|
||||
assert(oakengine_proxy_get_state(NULL) == OAKENGINE_PROXY_STATE_MISSING);
|
||||
assert(oakengine_proxy_get_state("") == OAKENGINE_PROXY_STATE_MISSING);
|
||||
assert(oakengine_proxy_get_state("/nonexistent/path/proxy.mp4") ==
|
||||
OAKENGINE_PROXY_STATE_MISSING);
|
||||
}
|
||||
|
||||
static void test_get_or_start_null(void)
|
||||
{
|
||||
oak_proxy_result result;
|
||||
memset(&result, 0xFF, sizeof(result));
|
||||
|
||||
// NULL cache_path should not crash; returns an error.
|
||||
assert(oakengine_proxy_get_or_start(NULL, NULL, 0, NULL, &result) !=
|
||||
OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_get_working_filename(void)
|
||||
{
|
||||
char buf[1024];
|
||||
int len = oakengine_proxy_get_working_filename("/tmp/test.proxy",
|
||||
buf, sizeof(buf));
|
||||
// Should return a filename derived from input, even if file doesn't exist.
|
||||
assert(len > 0);
|
||||
assert(strlen(buf) > 0);
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_proxy_get_working_filename(NULL, buf, sizeof(buf)) < 0);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_instance_lifecycle();
|
||||
test_params_from_config();
|
||||
test_state_string_round_trip();
|
||||
test_state_query();
|
||||
test_get_or_start_null();
|
||||
test_get_working_filename();
|
||||
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
return 0;
|
||||
}
|
||||
@@ -220,6 +220,16 @@ static void test_validation(OakEngineSequence *seq)
|
||||
oakengine_audio_free(NULL);
|
||||
}
|
||||
|
||||
static void test_render_cache_helpers(void)
|
||||
{
|
||||
// Without an active RenderManager, these return OAKENGINE_E_STATE rather
|
||||
// than crashing.
|
||||
assert(oakengine_render_cache_set_display_color_processor(NULL) ==
|
||||
OAKENGINE_E_STATE);
|
||||
assert(oakengine_render_cache_set_multicam_node(NULL) ==
|
||||
OAKENGINE_E_STATE);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
@@ -241,6 +251,7 @@ int main(void)
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Render");
|
||||
assert(seq != NULL);
|
||||
|
||||
test_render_cache_helpers();
|
||||
test_validation(seq);
|
||||
|
||||
// ---- GL-gated part ---------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
/***
|
||||
|
||||
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 tests for the liboakengine project serializer facade
|
||||
// (oakengine/serializer.h). Runs headless; no GPU required.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/node.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/serializer.h"
|
||||
#include "oakengine/viewer.h"
|
||||
|
||||
static void test_check_compressed_nonexistent(void)
|
||||
{
|
||||
assert(oakengine_serializer_check_compressed(
|
||||
"/nonexistent/path/project.ove") == 0);
|
||||
assert(oakengine_serializer_check_compressed(NULL) == 0);
|
||||
assert(oakengine_serializer_check_compressed("") == 0);
|
||||
}
|
||||
|
||||
static void test_clipboard_create_free(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
|
||||
OakEngineClipboard *cb =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb != NULL);
|
||||
|
||||
oakengine_clipboard_free(cb);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_copy_empty_nodes(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
|
||||
OakEngineClipboard *cb =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb != NULL);
|
||||
|
||||
// Copying zero nodes should not crash and should report success.
|
||||
assert(oakengine_clipboard_copy(cb) == OAKENGINE_OK);
|
||||
|
||||
oakengine_clipboard_free(cb);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_set_empty_sets(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
|
||||
OakEngineClipboard *cb =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb != NULL);
|
||||
|
||||
// Setting empty arrays (count=0, array=NULL) should be OK.
|
||||
assert(oakengine_clipboard_set_nodes(cb, NULL, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_set_markers(cb, NULL, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_set_keyframes(cb, NULL, 0) == OAKENGINE_OK);
|
||||
|
||||
// The clipboard with no content should still produce some XML.
|
||||
char buf[256];
|
||||
int len = oakengine_clipboard_save_to_xml(cb, buf, sizeof(buf));
|
||||
assert(len > 0);
|
||||
|
||||
oakengine_clipboard_free(cb);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_set_node_then_save_xml(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
OakEngineClipboard *cb =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb != NULL);
|
||||
|
||||
// Set one node on the clipboard.
|
||||
assert(oakengine_clipboard_set_nodes(cb, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
|
||||
|
||||
// Set a property on that node.
|
||||
assert(oakengine_clipboard_set_property(cb, solid, "pos_x", "100") ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
// save_to_xml should return a non-empty XML document.
|
||||
char buf[4096];
|
||||
int len = oakengine_clipboard_save_to_xml(cb, buf, sizeof(buf));
|
||||
assert(len > 0);
|
||||
assert(len < (int)sizeof(buf));
|
||||
// Should contain the node type id and the property.
|
||||
assert(strstr(buf, "solidgenerator") != NULL);
|
||||
assert(strstr(buf, "pos_x") != NULL);
|
||||
|
||||
// Query-length mode.
|
||||
int qlen = oakengine_clipboard_save_to_xml(cb, NULL, 0);
|
||||
assert(qlen == len);
|
||||
|
||||
oakengine_clipboard_free(cb);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_set_node_then_foreach_property(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
// Save_data: set node + property, then copy to system clipboard (save_data
|
||||
// is serialized). The paste result populates load_data, which is what
|
||||
// foreach_property reads.
|
||||
OakEngineClipboard *cb_copy =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb_copy != NULL);
|
||||
assert(oakengine_clipboard_set_nodes(cb_copy, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_set_property(cb_copy, solid, "pos_x", "100") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_set_property(cb_copy, solid, "pos_y", "200") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
|
||||
oakengine_clipboard_free(cb_copy);
|
||||
|
||||
OakEngineClipboard *cb_paste =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb_paste != NULL);
|
||||
|
||||
int result_code = -1;
|
||||
assert(oakengine_clipboard_paste(cb_paste, OAKENGINE_CLIPBOARD_NODES,
|
||||
project, &result_code, NULL, 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(result_code == OAKENGINE_SERIALIZER_OK);
|
||||
|
||||
// foreach_property should visit both pasted properties.
|
||||
int prop_seen = 0;
|
||||
int ret = oakengine_clipboard_foreach_property(
|
||||
cb_paste,
|
||||
[](OakEngineNode *node, const char *key, const char *value,
|
||||
void *userdata) -> int
|
||||
{
|
||||
(void) node;
|
||||
(void) key;
|
||||
(void) value;
|
||||
(*(int *) userdata)++;
|
||||
return 0;
|
||||
},
|
||||
&prop_seen);
|
||||
assert(ret == OAKENGINE_OK);
|
||||
assert(prop_seen >= 2);
|
||||
|
||||
oakengine_clipboard_free(cb_paste);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_clipboard_copy_paste_roundtrip(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
// Create clipboard A for copy (type doesn't matter; set_nodes overrides).
|
||||
OakEngineClipboard *cb_copy =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb_copy != NULL);
|
||||
assert(oakengine_clipboard_set_nodes(cb_copy, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
|
||||
oakengine_clipboard_free(cb_copy);
|
||||
|
||||
// Create clipboard B for paste.
|
||||
OakEngineClipboard *cb_paste =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb_paste != NULL);
|
||||
|
||||
int result_code = -1;
|
||||
int ret = oakengine_clipboard_paste(cb_paste, OAKENGINE_CLIPBOARD_NODES,
|
||||
project, &result_code, NULL, 0);
|
||||
assert(ret == OAKENGINE_OK);
|
||||
assert(result_code == OAKENGINE_SERIALIZER_OK);
|
||||
|
||||
// Verify loaded_* accessors.
|
||||
assert(oakengine_clipboard_get_loaded_node_count(cb_paste) == 1);
|
||||
OakEngineNode *loaded = oakengine_clipboard_get_loaded_node_at(cb_paste, 0);
|
||||
assert(loaded != NULL);
|
||||
assert(loaded != solid); // pasted node should be a new copy
|
||||
assert(oakengine_clipboard_get_loaded_node_at(cb_paste, -1) == NULL);
|
||||
assert(oakengine_clipboard_get_loaded_node_at(cb_paste, 1) == NULL);
|
||||
|
||||
oakengine_clipboard_free(cb_paste);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_clipboard_paste_with_map(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
OakEngineClipboard *cb_copy =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb_copy != NULL);
|
||||
assert(oakengine_clipboard_set_nodes(cb_copy, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
|
||||
oakengine_clipboard_free(cb_copy);
|
||||
|
||||
OakEngineClipboard *cb_paste =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb_paste != NULL);
|
||||
|
||||
int pair_count = 0;
|
||||
int result_code = -1;
|
||||
int ret = oakengine_clipboard_paste_with_map(
|
||||
cb_paste, OAKENGINE_CLIPBOARD_NODES, project,
|
||||
[](OakEngineNode *old_node, OakEngineNode *new_node,
|
||||
void *userdata) -> int
|
||||
{
|
||||
auto *pc = (int *) userdata;
|
||||
(*pc)++;
|
||||
assert(old_node != NULL);
|
||||
assert(new_node != NULL);
|
||||
assert(old_node != new_node);
|
||||
return 0;
|
||||
},
|
||||
&pair_count, &result_code, NULL, 0);
|
||||
assert(ret == OAKENGINE_OK);
|
||||
assert(result_code == OAKENGINE_SERIALIZER_OK);
|
||||
assert(pair_count == 1);
|
||||
|
||||
oakengine_clipboard_free(cb_paste);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_clipboard_foreach_iterators(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
// Copy with properties so the paste-result has properties, then verify
|
||||
// foreach_property, foreach_keyframe (should be 0) and foreach_connection
|
||||
// (should be 0 since nothing is connected).
|
||||
OakEngineClipboard *cb_copy =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb_copy != NULL);
|
||||
assert(oakengine_clipboard_set_nodes(cb_copy, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_set_property(cb_copy, solid, "pos_x", "50") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_set_property(cb_copy, solid, "pos_y", "75") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
|
||||
oakengine_clipboard_free(cb_copy);
|
||||
|
||||
OakEngineClipboard *cb_paste =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
|
||||
assert(cb_paste != NULL);
|
||||
|
||||
int result_code = -1;
|
||||
assert(oakengine_clipboard_paste(cb_paste, OAKENGINE_CLIPBOARD_NODES,
|
||||
project, &result_code, NULL, 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(result_code == OAKENGINE_SERIALIZER_OK);
|
||||
|
||||
// foreach_property should visit the pasted properties.
|
||||
int prop_count = 0;
|
||||
assert(oakengine_clipboard_foreach_property(
|
||||
cb_paste,
|
||||
[](OakEngineNode *node, const char *key, const char *value,
|
||||
void *userdata) -> int
|
||||
{
|
||||
(void) node;
|
||||
(void) key;
|
||||
(void) value;
|
||||
(*(int *) userdata)++;
|
||||
return 0;
|
||||
},
|
||||
&prop_count) == OAKENGINE_OK);
|
||||
assert(prop_count >= 2);
|
||||
|
||||
// foreach_keyframe should visit 0 (solid has no keyframe data in this test).
|
||||
int kf_count = 0;
|
||||
assert(oakengine_clipboard_foreach_keyframe(
|
||||
cb_paste,
|
||||
[](const char *node_id, OakEngineKeyframe *keyframe,
|
||||
void *userdata) -> int
|
||||
{
|
||||
(void) node_id;
|
||||
(void) keyframe;
|
||||
(*(int *) userdata)++;
|
||||
return 0;
|
||||
},
|
||||
&kf_count) == OAKENGINE_OK);
|
||||
|
||||
// foreach_connection should visit 0 (no connections copied).
|
||||
int conn_count = 0;
|
||||
assert(oakengine_clipboard_foreach_connection(
|
||||
cb_paste,
|
||||
[](OakEngineNode *output_node, OakEngineNode *input_node,
|
||||
const char *input_id, int element, void *userdata) -> int
|
||||
{
|
||||
(void) output_node;
|
||||
(void) input_node;
|
||||
(void) input_id;
|
||||
(void) element;
|
||||
(*(int *) userdata)++;
|
||||
return 0;
|
||||
},
|
||||
&conn_count) == OAKENGINE_OK);
|
||||
assert(conn_count == 0);
|
||||
|
||||
oakengine_clipboard_free(cb_paste);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_clipboard_marker_keyframe_accessors(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
// Create a sequence for its marker list.
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "MarkerSrc");
|
||||
assert(seq != NULL);
|
||||
|
||||
// Add a marker to the sequence's marker list.
|
||||
OakEngineMarkerList *list =
|
||||
oakengine_viewer_get_marker_list((OakEngineNode *)seq);
|
||||
assert(list != NULL);
|
||||
assert(oakengine_marker_list_add(list, 1, 1, 2, 1, "Test", 0) ==
|
||||
OAKENGINE_OK);
|
||||
OakEngineMarker *marker = oakengine_marker_list_at(list, 0);
|
||||
assert(marker != NULL);
|
||||
|
||||
// Copy markers to clipboard and save_to_xml (tests set_markers + save).
|
||||
OakEngineClipboard *cb_copy =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_MARKERS, project, NULL);
|
||||
assert(cb_copy != NULL);
|
||||
assert(oakengine_clipboard_set_markers(
|
||||
cb_copy, (const OakEngineMarker *const *)&marker, 1) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
|
||||
oakengine_clipboard_free(cb_copy);
|
||||
|
||||
// Paste back and verify get_loaded_marker accessors.
|
||||
OakEngineClipboard *cb_paste =
|
||||
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_MARKERS, project, NULL);
|
||||
assert(cb_paste != NULL);
|
||||
|
||||
int result_code = -1;
|
||||
assert(oakengine_clipboard_paste(cb_paste, OAKENGINE_CLIPBOARD_MARKERS,
|
||||
project, &result_code, NULL, 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(result_code == OAKENGINE_SERIALIZER_OK ||
|
||||
result_code == OAKENGINE_SERIALIZER_NO_DATA);
|
||||
// If paste succeeded, verify the accessors.
|
||||
if (result_code == OAKENGINE_SERIALIZER_OK) {
|
||||
int mc = oakengine_clipboard_get_loaded_marker_count(cb_paste);
|
||||
assert(mc >= 0);
|
||||
OakEngineMarker *pm = oakengine_clipboard_get_loaded_marker_at(
|
||||
cb_paste, 0);
|
||||
if (pm != NULL) {
|
||||
assert(oakengine_clipboard_get_loaded_marker_at(cb_paste, -1) ==
|
||||
NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// get_loaded_keyframe accessors with 0 keyframes (no keyframes copied).
|
||||
assert(oakengine_clipboard_get_loaded_keyframe_count(cb_paste) >= 0);
|
||||
assert(oakengine_clipboard_get_loaded_keyframe_at(cb_paste, 0) == NULL);
|
||||
|
||||
oakengine_clipboard_free(cb_paste);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_check_compressed_nonexistent();
|
||||
test_clipboard_create_free();
|
||||
test_copy_empty_nodes();
|
||||
test_set_empty_sets();
|
||||
test_set_node_then_save_xml();
|
||||
test_set_node_then_foreach_property();
|
||||
test_clipboard_copy_paste_roundtrip();
|
||||
test_clipboard_paste_with_map();
|
||||
test_clipboard_foreach_iterators();
|
||||
test_clipboard_marker_keyframe_accessors();
|
||||
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/***
|
||||
|
||||
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 sync facade. The validation part
|
||||
// (handle checking, not-initialized errors) requires no GL and must
|
||||
// always pass. The estimation part renders the clips' audio, so it is
|
||||
// GL-gated like oakengine_playback_test (dynamic backend probe +
|
||||
// worker binary, SKIP with exit 0 when unavailable).
|
||||
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdint.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 <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "oakengine/footage.h"
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/sync.h"
|
||||
#include "oakengine/timeline.h"
|
||||
#include "render/backend/dynamicrenderer.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_sync_test_%lu", base,
|
||||
(unsigned long)GetCurrentProcessId());
|
||||
assert(_mkdir(g_tmpdir) == 0);
|
||||
#else
|
||||
strcpy(g_tmpdir, "/tmp/oakengine_sync_test_XXXXXX");
|
||||
assert(mkdtemp(g_tmpdir) != NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Same probe as tests/gtest/render_worker_footage_test.cpp.
|
||||
static bool is_render_backend_available(const QString &backend)
|
||||
{
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
olive::DynamicRenderer renderer(backend);
|
||||
if (!renderer.load()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OakRenderBackendInfo info = {};
|
||||
if (!renderer.get_backend_info(&info)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (backend == QStringLiteral("opengl") &&
|
||||
info.kind != oak_render_backend_opengl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return renderer.init();
|
||||
#else
|
||||
Q_UNUSED(backend)
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool worker_binary_exists()
|
||||
{
|
||||
QDir dir(QCoreApplication::applicationDirPath());
|
||||
dir.cd(QStringLiteral("../worker"));
|
||||
#if defined(_WIN32)
|
||||
return QFileInfo::exists(dir.filePath(QStringLiteral("oak-render-worker.exe")));
|
||||
#else
|
||||
return QFileInfo::exists(dir.filePath(QStringLiteral("oak-render-worker")));
|
||||
#endif
|
||||
}
|
||||
|
||||
// demo.mp4's audio is near-silent and useless for correlation, and
|
||||
// stationary noise or a constant chirp both have a flat RMS envelope
|
||||
// (no lag peak). Write noise with a deterministic per-window random
|
||||
// gain: a textured, unique envelope for both the lag and rate search.
|
||||
static void write_textured_wav(const QString &path, int seconds)
|
||||
{
|
||||
const int rate = 48000;
|
||||
const int channels = 2;
|
||||
const int frames = rate * seconds;
|
||||
const int data_size = frames * channels * int(sizeof(int16_t));
|
||||
const int block = rate / 20; // one gain value per envelope window
|
||||
|
||||
QFile f(path);
|
||||
assert(f.open(QFile::WriteOnly));
|
||||
auto write_u32 = [&f](uint32_t v) {
|
||||
f.write(reinterpret_cast<const char *>(&v), 4);
|
||||
};
|
||||
auto write_u16 = [&f](uint16_t v) {
|
||||
f.write(reinterpret_cast<const char *>(&v), 2);
|
||||
};
|
||||
|
||||
f.write("RIFF", 4);
|
||||
write_u32(uint32_t(36 + data_size));
|
||||
f.write("WAVE", 4);
|
||||
f.write("fmt ", 4);
|
||||
write_u32(16);
|
||||
write_u16(1); // PCM
|
||||
write_u16(uint16_t(channels));
|
||||
write_u32(uint32_t(rate));
|
||||
write_u32(uint32_t(rate * channels * int(sizeof(int16_t))));
|
||||
write_u16(uint16_t(channels * int(sizeof(int16_t))));
|
||||
write_u16(16);
|
||||
f.write("data", 4);
|
||||
write_u32(uint32_t(data_size));
|
||||
|
||||
uint32_t state = 0x12345678u;
|
||||
auto next_u32 = [&state]() {
|
||||
state = state * 1664525u + 1013904223u;
|
||||
return state;
|
||||
};
|
||||
|
||||
const int blocks = frames / block + 2;
|
||||
std::vector<double> block_gains(static_cast<size_t>(blocks));
|
||||
for (int b = 0; b < blocks; b++) {
|
||||
block_gains[size_t(b)] =
|
||||
0.1 + 0.9 * double(next_u32() % 1000) / 1000.0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < frames; i++) {
|
||||
// Constant gain within a block: the envelope window equals the
|
||||
// block, so envelope[b] == block_gains[b] (sharp and unique).
|
||||
const double gain = block_gains[size_t(i / block)];
|
||||
const int16_t sample = int16_t(
|
||||
(int(next_u32() >> 16) % 32768 - 16384) * gain);
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
f.write(reinterpret_cast<const char *>(&sample), 2);
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
// The shared fixture: one sequence with an audio track and two clips of
|
||||
// the noise footage; the target's content starts k_offset_frames later
|
||||
// in the source (the application's real sync scenario: two recordings
|
||||
// of one event, one started late). The sequence runs at 20 fps so one
|
||||
// frame is exactly one envelope window (1/20 s).
|
||||
static const int64_t k_offset_frames = 8;
|
||||
|
||||
static OakEngineClip *make_pair(OakEngineProject *project,
|
||||
OakEngineSequence *seq,
|
||||
const char *media_path,
|
||||
OakEngineClip **target_out)
|
||||
{
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
|
||||
0);
|
||||
// A second audio track: placing the target on the SAME track would
|
||||
// overwrite (trim) the reference clip.
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
|
||||
1);
|
||||
OakEngineFootage *footage =
|
||||
oakengine_project_import_footage(project, media_path);
|
||||
assert(footage != NULL);
|
||||
OakEngineClip *reference = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_AUDIO, 0, 0, 160, 0);
|
||||
assert(reference != NULL);
|
||||
OakEngineClip *target = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_AUDIO, 1, k_offset_frames,
|
||||
160 + k_offset_frames, k_offset_frames);
|
||||
assert(target != NULL);
|
||||
oakengine_footage_free(footage);
|
||||
*target_out = target;
|
||||
return reference;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
|
||||
// Sandbox the config/cache/data locations (see oakengine_init_test).
|
||||
#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
|
||||
|
||||
// HEADLESS is enough for the validation part.
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Sync");
|
||||
assert(seq != NULL);
|
||||
// 20 fps: one frame == one envelope window (1/20 s) exactly.
|
||||
assert(oakengine_sequence_set_video_params(seq, -1, -1, 20, 1, -1, -1,
|
||||
-1, -1, 1) == OAKENGINE_OK);
|
||||
|
||||
const QString noise_path = QDir(QString::fromUtf8(g_tmpdir))
|
||||
.filePath(QStringLiteral("sync-noise.wav"));
|
||||
write_textured_wav(noise_path, 8);
|
||||
OakEngineClip *target = NULL;
|
||||
OakEngineClip *reference =
|
||||
make_pair(project, seq, noise_path.toUtf8().constData(), &target);
|
||||
|
||||
// ---- Validation (no GL) -------------------------------------------
|
||||
double offset_s = -1, confidence = -1, stretch = -1;
|
||||
assert(oakengine_sync_estimate_offset(NULL, reference, target,
|
||||
&offset_s,
|
||||
&confidence) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_sync_estimate_offset(seq, NULL, target, &offset_s,
|
||||
&confidence) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_sync_estimate_offset(seq, reference, NULL, &offset_s,
|
||||
&confidence) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_sync_estimate_stretch_offset(NULL, reference, target,
|
||||
&stretch, &offset_s,
|
||||
&confidence) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
char err[256];
|
||||
assert(oakengine_sync_last_error(err, sizeof(err)) > 0);
|
||||
|
||||
// Valid handles but the engine lacks the RENDER bit: OAKENGINE_E_STATE
|
||||
// with a readable reason, nothing else changed.
|
||||
assert(oakengine_sync_estimate_offset(seq, reference, target, &offset_s,
|
||||
&confidence) == OAKENGINE_E_STATE);
|
||||
assert(oakengine_sync_last_error(err, sizeof(err)) > 0);
|
||||
assert(strstr(err, "OAKENGINE_INIT_RENDER") != NULL);
|
||||
|
||||
// ---- GL-gated estimation ------------------------------------------
|
||||
if (!is_render_backend_available(QStringLiteral("opengl"))) {
|
||||
printf("oakengine_sync_test: SKIP: OpenGL render backend not "
|
||||
"available, estimation assertions skipped\n");
|
||||
oakengine_project_free(project);
|
||||
oakengine_shutdown();
|
||||
return 0;
|
||||
}
|
||||
if (!worker_binary_exists()) {
|
||||
printf("oakengine_sync_test: SKIP: oak-render-worker binary not "
|
||||
"found, estimation assertions skipped\n");
|
||||
oakengine_project_free(project);
|
||||
oakengine_shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
olive::Config::current()[QStringLiteral("GraphicsBackend")] =
|
||||
QStringLiteral("opengl");
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS | OAKENGINE_INIT_RENDER) ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
// The target's content starts k_offset_frames later in the source:
|
||||
// the estimator must report that offset back (negative = move the
|
||||
// target earlier). At 20 fps one frame is one envelope window, so
|
||||
// the expected value is exact; the tolerance is one window (the
|
||||
// method's quantization).
|
||||
const double expected_s = double(k_offset_frames) / 20.0;
|
||||
const double tolerance_s = 1.0 / 20.0;
|
||||
|
||||
const int est_rc = oakengine_sync_estimate_offset(
|
||||
seq, reference, target, &offset_s, &confidence);
|
||||
if (est_rc != OAKENGINE_OK) {
|
||||
char est_err[512];
|
||||
est_err[0] = '\0';
|
||||
oakengine_sync_last_error(est_err, sizeof(est_err));
|
||||
fprintf(stderr, "DEBUG est_rc=%d off=%f conf=%f err='%s'\n", est_rc,
|
||||
offset_s, confidence, est_err);
|
||||
}
|
||||
assert(est_rc == OAKENGINE_OK);
|
||||
assert(fabs(fabs(offset_s) - expected_s) < tolerance_s);
|
||||
assert(offset_s < 0.0); // the target is delayed: it must move earlier
|
||||
assert(confidence > 0.0 && confidence <= 1.0);
|
||||
|
||||
// Same-speed content: the stretch estimator reports rate ~1 and the
|
||||
// same offset.
|
||||
const int str_rc = oakengine_sync_estimate_stretch_offset(
|
||||
seq, reference, target, &stretch, &offset_s, &confidence);
|
||||
assert(str_rc == OAKENGINE_OK);
|
||||
assert(fabs(stretch - 1.0) < 0.01);
|
||||
assert(fabs(fabs(offset_s) - expected_s) < tolerance_s);
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
printf("oakengine_sync_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
/***
|
||||
|
||||
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 tests for the liboakengine task and undo families
|
||||
// (oakengine/task.h and oakengine/undo.h). Runs headless; no GPU required.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include "oakengine/events.h"
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/task.h"
|
||||
#include "oakengine/undo.h"
|
||||
|
||||
static int g_task_started = 0;
|
||||
static int g_task_progress = 0;
|
||||
static int g_task_finished = 0;
|
||||
static int g_task_succeeded = 0;
|
||||
static int g_manager_added = 0;
|
||||
static int g_manager_removed = 0;
|
||||
|
||||
static void task_event_cb(const oakengine_event *event, void *userdata)
|
||||
{
|
||||
(void) userdata;
|
||||
switch (event->id) {
|
||||
case OAKENGINE_EVENT_TASK_STARTED:
|
||||
g_task_started = 1;
|
||||
break;
|
||||
case OAKENGINE_EVENT_TASK_PROGRESS:
|
||||
g_task_progress = 1;
|
||||
break;
|
||||
case OAKENGINE_EVENT_TASK_FINISHED:
|
||||
g_task_finished = 1;
|
||||
g_task_succeeded = (int) event->a;
|
||||
break;
|
||||
case OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED:
|
||||
g_manager_added = 1;
|
||||
break;
|
||||
case OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED:
|
||||
g_manager_removed = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void test_manager_no_engine(void)
|
||||
{
|
||||
assert(oakengine_task_manager_handle() == NULL);
|
||||
assert(oakengine_task_manager_count() == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_task_manager_first() == NULL);
|
||||
assert(oakengine_task_manager_add(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_task_manager_cancel(NULL) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_manager_empty(void)
|
||||
{
|
||||
void *mgr = oakengine_task_manager_handle();
|
||||
assert(mgr != NULL);
|
||||
assert(oakengine_task_manager_count() == 0);
|
||||
assert(oakengine_task_manager_first() == NULL);
|
||||
}
|
||||
|
||||
static void test_task_null(void)
|
||||
{
|
||||
char buf[64];
|
||||
assert(oakengine_task_title(NULL, buf, sizeof(buf)) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_task_error(NULL, buf, sizeof(buf)) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_task_start_time(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_task_is_cancelled(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_task_cancel(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_task_start_sync(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_task_free(NULL) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_import_error_path(void)
|
||||
{
|
||||
OakEngineProject *p = oakengine_project_create();
|
||||
assert(p != NULL);
|
||||
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||
OakEngineNode *root = oakengine_project_root(p);
|
||||
assert(root != NULL);
|
||||
|
||||
// Empty URL list is rejected at creation.
|
||||
OakEngineTask *task = oakengine_task_create_project_import(root, NULL, 0);
|
||||
assert(task == NULL);
|
||||
|
||||
// Valid creation but with non-existent file gives zero footage/one invalid.
|
||||
const char *url = "file:///this/file/does/not/exist.mov";
|
||||
task = oakengine_task_create_project_import(root, &url, 1);
|
||||
assert(task != NULL);
|
||||
|
||||
assert(oakengine_task_import_file_count(task) == 1);
|
||||
|
||||
int result = oakengine_task_start_sync(task);
|
||||
(void) result;
|
||||
|
||||
assert(oakengine_task_import_footage_count(task) == 0);
|
||||
assert(oakengine_task_import_invalid_files_count(task) == 1);
|
||||
char buf[256];
|
||||
int len = oakengine_task_import_invalid_file_at(task, 0, buf, sizeof(buf));
|
||||
assert(len > 0);
|
||||
assert(strstr(buf, "exist.mov") != NULL);
|
||||
assert(oakengine_task_import_invalid_file_at(task, 0, NULL, 0) == len);
|
||||
assert(oakengine_task_import_invalid_file_at(task, 1, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_task_free(task) == OAKENGINE_OK);
|
||||
oakengine_project_free(p);
|
||||
}
|
||||
|
||||
static void test_load_task_sync(void)
|
||||
{
|
||||
OakEngineTask *task =
|
||||
oakengine_task_create_project_load("/no/such/project.ove");
|
||||
assert(task != NULL);
|
||||
|
||||
// No event subscription here; just confirm it reports failure cleanly.
|
||||
int ok = oakengine_task_start_sync(task);
|
||||
assert(ok == 0);
|
||||
|
||||
char err[256];
|
||||
int len = oakengine_task_error(task, err, sizeof(err));
|
||||
assert(len > 0);
|
||||
|
||||
assert(oakengine_task_free(task) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_task_events(void)
|
||||
{
|
||||
OakEngineProject *p = oakengine_project_create();
|
||||
assert(p != NULL);
|
||||
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||
OakEngineNode *root = oakengine_project_root(p);
|
||||
assert(root != NULL);
|
||||
|
||||
const char *url = "file:///this/file/does/not/exist.mov";
|
||||
OakEngineTask *task =
|
||||
oakengine_task_create_project_import(root, &url, 1);
|
||||
assert(task != NULL);
|
||||
|
||||
void *mgr = oakengine_task_manager_handle();
|
||||
int64_t sub_started = oakengine_event_subscribe(
|
||||
task, OAKENGINE_EVENT_TASK_STARTED, task_event_cb, NULL);
|
||||
int64_t sub_progress = oakengine_event_subscribe(
|
||||
task, OAKENGINE_EVENT_TASK_PROGRESS, task_event_cb, NULL);
|
||||
int64_t sub_finished = oakengine_event_subscribe(
|
||||
task, OAKENGINE_EVENT_TASK_FINISHED, task_event_cb, NULL);
|
||||
int64_t sub_added = oakengine_event_subscribe(
|
||||
mgr, OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED, task_event_cb, NULL);
|
||||
int64_t sub_removed = oakengine_event_subscribe(
|
||||
mgr, OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED, task_event_cb, NULL);
|
||||
|
||||
assert(sub_started > 0);
|
||||
assert(sub_progress > 0);
|
||||
assert(sub_finished > 0);
|
||||
assert(sub_added > 0);
|
||||
assert(sub_removed > 0);
|
||||
|
||||
g_task_started = g_task_progress = g_task_finished = 0;
|
||||
g_task_succeeded = g_manager_added = g_manager_removed = 0;
|
||||
|
||||
assert(oakengine_task_manager_add(task) == OAKENGINE_OK);
|
||||
|
||||
// Wait for the task to finish. Manager tasks run on a worker thread and
|
||||
// emit events on that thread; spin briefly until the finished event fires.
|
||||
for (int i = 0; i < 200 && !g_task_finished; i++) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
assert(g_manager_added == 1);
|
||||
assert(g_task_started == 1);
|
||||
assert(g_task_finished == 1);
|
||||
// The import task may succeed even when all files are invalid; the event
|
||||
// payload only reports Task::finished() success, which is implementation
|
||||
// dependent. We only verify the event fired and had a boolean value.
|
||||
assert(g_task_succeeded == 0 || g_task_succeeded == 1);
|
||||
|
||||
// Cancel returns OK whether the task is still running or already done.
|
||||
assert(oakengine_task_manager_cancel(task) == OAKENGINE_OK);
|
||||
|
||||
oakengine_event_unsubscribe(sub_started);
|
||||
oakengine_event_unsubscribe(sub_progress);
|
||||
oakengine_event_unsubscribe(sub_finished);
|
||||
oakengine_event_unsubscribe(sub_added);
|
||||
oakengine_event_unsubscribe(sub_removed);
|
||||
|
||||
oakengine_project_free(p);
|
||||
}
|
||||
|
||||
static void test_undo_round_trip(void)
|
||||
{
|
||||
assert(oakengine_undo_handle() != NULL);
|
||||
assert(oakengine_undo_count() == 1); // the empty "New Project" entry
|
||||
assert(oakengine_undo_index() == 1);
|
||||
assert(oakengine_undo_can_undo() == 0);
|
||||
assert(oakengine_undo_can_redo() == 0);
|
||||
|
||||
char text[256];
|
||||
int len = oakengine_undo_command_text(0, text, sizeof(text));
|
||||
assert(len > 0);
|
||||
|
||||
// Push a custom no-op command with a user-visible label.
|
||||
void *cmd = oakengine_undo_command_create(
|
||||
"Internal Name", NULL, NULL, NULL, NULL);
|
||||
assert(cmd != NULL);
|
||||
assert(oakengine_undo_push(cmd, "Test Command") == OAKENGINE_OK);
|
||||
assert(oakengine_undo_count() == 2);
|
||||
assert(oakengine_undo_index() == 2);
|
||||
assert(oakengine_undo_can_undo() == 1);
|
||||
|
||||
len = oakengine_undo_command_text(1, text, sizeof(text));
|
||||
assert(len > 0);
|
||||
assert(strstr(text, "Test Command") != NULL);
|
||||
|
||||
assert(oakengine_undo_command_is_done(1) == 1);
|
||||
assert(oakengine_undo_jump(1) == OAKENGINE_OK);
|
||||
assert(oakengine_undo_index() == 1);
|
||||
assert(oakengine_undo_can_redo() == 1);
|
||||
assert(oakengine_undo_command_is_done(1) == 0);
|
||||
|
||||
assert(oakengine_undo_jump(2) == OAKENGINE_OK);
|
||||
assert(oakengine_undo_index() == 2);
|
||||
assert(oakengine_undo_can_undo() == 1);
|
||||
|
||||
assert(oakengine_undo_clear() == OAKENGINE_OK);
|
||||
assert(oakengine_undo_count() == 1);
|
||||
assert(oakengine_undo_index() == 1);
|
||||
assert(oakengine_undo_can_undo() == 0);
|
||||
}
|
||||
|
||||
static void test_custom_command_multi(void)
|
||||
{
|
||||
static int g_redo = 0;
|
||||
static int g_undo = 0;
|
||||
static int g_free = 0;
|
||||
|
||||
g_redo = g_undo = g_free = 0;
|
||||
|
||||
void *cmd = oakengine_undo_command_create(
|
||||
"Custom",
|
||||
[](void *ud) { (void) ud; g_redo++; },
|
||||
[](void *ud) { (void) ud; g_undo++; },
|
||||
[](void *ud) { (void) ud; g_free++; },
|
||||
NULL);
|
||||
assert(cmd != NULL);
|
||||
|
||||
assert(oakengine_undo_command_redo_now(cmd) == OAKENGINE_OK);
|
||||
assert(g_redo == 1);
|
||||
assert(g_undo == 0);
|
||||
|
||||
assert(oakengine_undo_command_undo_now(cmd) == OAKENGINE_OK);
|
||||
assert(g_undo == 1);
|
||||
|
||||
void *multi = oakengine_undo_command_create_multi();
|
||||
assert(multi != NULL);
|
||||
assert(oakengine_undo_command_multi_child_count(multi) == 0);
|
||||
assert(oakengine_undo_command_multi_add_child(multi, cmd) == OAKENGINE_OK);
|
||||
assert(oakengine_undo_command_multi_child_count(multi) == 1);
|
||||
assert(oakengine_undo_command_multi_add_child(multi, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_undo_command_multi_add_child(NULL, cmd) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Remove the child from the multi-command and free it directly to verify
|
||||
// the custom command's free callback. (MultiUndoCommand does not own its
|
||||
// children, so freeing the multi-command alone would leak the child.)
|
||||
assert(oakengine_undo_command_multi_child_count(multi) == 1);
|
||||
oakengine_undo_command_free(cmd);
|
||||
assert(g_free == 1);
|
||||
|
||||
// The now-empty multi-command can be freed safely.
|
||||
oakengine_undo_command_free(multi);
|
||||
}
|
||||
|
||||
// ---- Save task ---------------------------------------------------------------
|
||||
|
||||
static void test_save_task_creation(void)
|
||||
{
|
||||
OakEngineProject *p = oakengine_project_create();
|
||||
assert(p != NULL);
|
||||
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||
|
||||
// Create a save task with NULL override and NULL layout.
|
||||
OakEngineTask *task = oakengine_task_create_project_save(
|
||||
p, 1, NULL, NULL);
|
||||
assert(task != NULL);
|
||||
|
||||
// task_save_get_project should return the project we passed.
|
||||
assert(oakengine_task_save_get_project(task) == p);
|
||||
assert(oakengine_task_save_get_project(NULL) == NULL);
|
||||
|
||||
// Running sync on an untitled project: may succeed or fail gracefully.
|
||||
int ok = oakengine_task_start_sync(task);
|
||||
(void) ok; // must not crash
|
||||
|
||||
assert(oakengine_task_free(task) == OAKENGINE_OK);
|
||||
oakengine_project_free(p);
|
||||
}
|
||||
|
||||
// ---- OTIO / Export / Proxy task creators (null/invalid smoke) ----------------
|
||||
|
||||
static void test_other_task_creation(void)
|
||||
{
|
||||
// OTIO load: test that it either returns NULL (no OTIO support) or
|
||||
// creates a task that can be freed.
|
||||
OakEngineTask *task = oakengine_task_create_project_load_otio(
|
||||
"/nonexistent.otio");
|
||||
if (task != NULL) {
|
||||
assert(oakengine_task_free(task) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
// OTIO save: same.
|
||||
task = oakengine_task_create_project_save_otio(NULL);
|
||||
// Passing NULL project may return NULL.
|
||||
|
||||
// Export: NULL sequence, NULL params.
|
||||
assert(oakengine_task_create_export(NULL, NULL) == NULL);
|
||||
|
||||
// Proxy: NULL footage.
|
||||
assert(oakengine_task_create_proxy(NULL) == NULL);
|
||||
}
|
||||
|
||||
// ---- Import result accessors (extending test_import_error_path) --------------
|
||||
|
||||
static void test_import_result_accessors(void)
|
||||
{
|
||||
OakEngineProject *p = oakengine_project_create();
|
||||
assert(p != NULL);
|
||||
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||
OakEngineNode *root = oakengine_project_root(p);
|
||||
assert(root != NULL);
|
||||
|
||||
const char *url = "file:///this/file/does/not/exist.mov";
|
||||
OakEngineTask *task = oakengine_task_create_project_import(root, &url, 1);
|
||||
assert(task != NULL);
|
||||
|
||||
int ok = oakengine_task_start_sync(task);
|
||||
assert(ok == 0 || ok == 1);
|
||||
(void) ok;
|
||||
|
||||
// Import of invalid file: footage_at should return 0/NULL.
|
||||
assert(oakengine_task_import_footage_count(task) == 0);
|
||||
assert(oakengine_task_import_footage_at(task, 0) == NULL);
|
||||
assert(oakengine_task_import_footage_at(task, -1) == NULL);
|
||||
assert(oakengine_task_import_footage_at(NULL, 0) == NULL);
|
||||
|
||||
// import_get_command: should be non-NULL (the import built a command
|
||||
// even when all files failed) or NULL (no data to build).
|
||||
void *cmd = oakengine_task_import_get_command(task);
|
||||
if (cmd != NULL) {
|
||||
oakengine_undo_command_free(cmd);
|
||||
}
|
||||
|
||||
assert(oakengine_task_free(task) == OAKENGINE_OK);
|
||||
oakengine_project_free(p);
|
||||
}
|
||||
|
||||
// ---- Undo action helpers -----------------------------------------------------
|
||||
|
||||
static void test_undo_actions(void)
|
||||
{
|
||||
// update_actions should not crash.
|
||||
assert(oakengine_undo_update_actions() == OAKENGINE_OK);
|
||||
|
||||
// undo_action / redo_action return QAction* as void* (may be NULL).
|
||||
oakengine_undo_undo_action();
|
||||
oakengine_undo_redo_action();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_manager_no_engine();
|
||||
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
test_manager_empty();
|
||||
test_task_null();
|
||||
test_import_error_path();
|
||||
test_load_task_sync();
|
||||
test_task_events();
|
||||
test_undo_round_trip();
|
||||
test_custom_command_multi();
|
||||
test_save_task_creation();
|
||||
test_other_task_creation();
|
||||
test_import_result_accessors();
|
||||
test_undo_actions();
|
||||
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
return 0;
|
||||
}
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "oakengine/node.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/timeline.h"
|
||||
#include "oakengine/viewer.h"
|
||||
|
||||
#ifndef OAK_TEST_SOURCE_DIR
|
||||
#define OAK_TEST_SOURCE_DIR "."
|
||||
@@ -1205,6 +1206,596 @@ static void test_batch_editing_round3(const char *media_path)
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_sequence_clip(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Outer");
|
||||
assert(seq != NULL);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
0);
|
||||
OakEngineSequence *nested = oakengine_sequence_new(project, "Nested");
|
||||
assert(nested != NULL);
|
||||
|
||||
int64_t in = -1, out = -1, media_in = -1;
|
||||
|
||||
// Place the nested sequence as a clip; undo/redo ride the stack.
|
||||
OakEngineClip *clip = oakengine_sequence_add_sequence_clip(
|
||||
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 10, 40, 5);
|
||||
assert(clip != NULL);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 1);
|
||||
assert(oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
0) == clip);
|
||||
assert(oakengine_clip_get_range(clip, &in, &out, &media_in) ==
|
||||
OAKENGINE_OK);
|
||||
assert(in == 10 && out == 40 && media_in == 5);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
|
||||
// A sequence cannot nest into itself or into a sequence that
|
||||
// (indirectly) receives it: place Outer into Nested first, then
|
||||
// placing Nested into Outer must be refused, all without side
|
||||
// effects.
|
||||
assert(oakengine_sequence_add_track(nested, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
0);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
nested, seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) != NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
|
||||
char err[256];
|
||||
assert(oakengine_sequence_last_error(err, sizeof(err)) > 0);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 0);
|
||||
|
||||
// Validation: cross-project, bad track type/index and bad ranges are
|
||||
// all rejected without side effects.
|
||||
OakEngineProject *other = oakengine_project_create();
|
||||
assert(other != NULL);
|
||||
assert(oakengine_project_new(other) == OAKENGINE_OK);
|
||||
OakEngineSequence *foreign = oakengine_sequence_new(other, "Foreign");
|
||||
assert(foreign != NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, foreign, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
|
||||
oakengine_project_free(other);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, nested, OAKENGINE_TRACK_TYPE_SUBTITLE, 0, 0, 10,
|
||||
0) == NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 5, 0, 10, 0) == NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 10, 10, 0) == NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, -1, 10, 0) == NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, -1) == NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
NULL, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
|
||||
assert(oakengine_sequence_add_sequence_clip(
|
||||
seq, NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 0);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// Track queries: oakengine_track_type / oakengine_track_get_length /
|
||||
// oakengine_track_is_range_free / oakengine_track_height_interval /
|
||||
// oakengine_track_height_minimum.
|
||||
static void test_track_queries(const char *media_path)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Queries");
|
||||
assert(seq != NULL);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
0);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
|
||||
0);
|
||||
|
||||
OakEngineFootage *footage =
|
||||
oakengine_project_import_footage(project, media_path);
|
||||
assert(footage != NULL);
|
||||
// Clip at [10, 20) on the video track.
|
||||
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 10, 20, 0);
|
||||
assert(clip != NULL);
|
||||
|
||||
// Type through the opaque handle.
|
||||
assert(oakengine_track_type(NULL) == -1);
|
||||
OakEngineTrack *vtrack = oakengine_sequence_track_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0);
|
||||
OakEngineTrack *atrack = oakengine_sequence_track_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_AUDIO, 0);
|
||||
assert(vtrack != NULL && atrack != NULL);
|
||||
assert(oakengine_track_type(vtrack) == OAKENGINE_TRACK_TYPE_VIDEO);
|
||||
assert(oakengine_track_type(atrack) == OAKENGINE_TRACK_TYPE_AUDIO);
|
||||
assert(oakengine_sequence_track_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 5) ==
|
||||
NULL);
|
||||
assert(oakengine_sequence_track_at(NULL, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == NULL);
|
||||
|
||||
// Length: the video track ends at 20, the empty audio track at 0.
|
||||
int64_t length = -1;
|
||||
assert(oakengine_track_get_length(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
&length) == OAKENGINE_OK);
|
||||
assert(length == 20);
|
||||
assert(oakengine_track_get_length(seq, OAKENGINE_TRACK_TYPE_AUDIO, 0,
|
||||
&length) == OAKENGINE_OK);
|
||||
assert(length == 0);
|
||||
assert(oakengine_track_get_length(seq, OAKENGINE_TRACK_TYPE_VIDEO, 5,
|
||||
&length) == OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_track_get_length(NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
&length) == OAKENGINE_E_INVALID);
|
||||
|
||||
// Range free: [0, 10) and [20, 30) are free, [15, 25) intersects the
|
||||
// clip, a zero-length probe at 15 also intersects.
|
||||
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
0, 10) == 1);
|
||||
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
20, 30) == 1);
|
||||
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
15, 25) == 0);
|
||||
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_AUDIO, 0,
|
||||
15, 25) == 1);
|
||||
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 5,
|
||||
0, 10) == OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
10, 5) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_track_is_range_free(NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||
0, 10) == OAKENGINE_E_INVALID);
|
||||
|
||||
// Height constants are positive (minimum 1.5, interval 0.5 in the
|
||||
// engine; only positivity is contract-level).
|
||||
assert(oakengine_track_height_interval() > 0.0);
|
||||
assert(oakengine_track_height_minimum() > 0.0);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// ---- Marker handle family (B4c) ----------------------------------------------
|
||||
|
||||
static void test_marker_handle_family(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "MarkerHandles");
|
||||
assert(seq != NULL);
|
||||
|
||||
OakEngineMarkerList *list =
|
||||
oakengine_viewer_get_marker_list((OakEngineNode *)seq);
|
||||
assert(list != NULL);
|
||||
assert(oakengine_viewer_get_marker_list(NULL) == NULL);
|
||||
assert(oakengine_marker_list_count(list) == 0);
|
||||
assert(oakengine_marker_list_count(NULL) == 0);
|
||||
|
||||
// Add two markers (rational seconds) through the list family.
|
||||
assert(oakengine_marker_list_add(list, 4, 1, 6, 1, "Out", 2) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_marker_list_add(list, 1, 1, 2, 1, "In", 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_marker_list_count(list) == 2);
|
||||
|
||||
// Sorted by time: index 0 is the 1s marker.
|
||||
OakEngineMarker *m0 = oakengine_marker_list_at(list, 0);
|
||||
OakEngineMarker *m1 = oakengine_marker_list_at(list, 1);
|
||||
assert(m0 != NULL && m1 != NULL && m0 != m1);
|
||||
assert(oakengine_marker_list_at(list, 2) == NULL);
|
||||
assert(oakengine_marker_list_at(list, -1) == NULL);
|
||||
|
||||
int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0;
|
||||
assert(oakengine_marker_get_time(m0, &in_num, &in_den, &out_num,
|
||||
&out_den) == OAKENGINE_OK);
|
||||
assert(in_num == 1 && in_den == 1 && out_num == 2 && out_den == 1);
|
||||
char name[64];
|
||||
assert(oakengine_marker_get_name(m0, name, sizeof(name)) == 2);
|
||||
assert(strcmp(name, "In") == 0);
|
||||
assert(oakengine_marker_get_color(m0) == 0);
|
||||
assert(oakengine_marker_get_color(m1) == 2);
|
||||
|
||||
// Lookup by exact in-point.
|
||||
assert(oakengine_marker_list_marker_at_time(list, 4, 1) == m1);
|
||||
assert(oakengine_marker_list_marker_at_time(list, 5, 1) == NULL);
|
||||
|
||||
// Sibling check: m0 has a sibling at 4s (m1), none at 3s.
|
||||
assert(oakengine_marker_has_sibling_at_time(m0, 4, 1) == 1);
|
||||
assert(oakengine_marker_has_sibling_at_time(m0, 3, 1) == 0);
|
||||
|
||||
// Live (non-undo) resize, then the undoable commit with the old range.
|
||||
assert(oakengine_marker_set_time_live(m0, 1, 1, 3, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_get_time(m0, NULL, NULL, &out_num, &out_den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(out_num == 3 && out_den == 1);
|
||||
assert(oakengine_marker_commit_time(m0, 1, 1, 3, 1, 1, 1, 2, 1,
|
||||
NULL) == OAKENGINE_OK);
|
||||
// Undo restores the pre-commit (live) state is NOT reverted (the live
|
||||
// edit was already applied; undo goes back to the old range).
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_get_time(m0, NULL, NULL, &out_num, &out_den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(out_num == 2 && out_den == 1);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_get_time(m0, NULL, NULL, &out_num, &out_den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(out_num == 3 && out_den == 1);
|
||||
|
||||
// Detached marker creation (used by the UI before adding to a list).
|
||||
OakEngineMarker *detached = oakengine_marker_create(3, 5, 1, 7, 1,
|
||||
"Detached");
|
||||
assert(detached != NULL);
|
||||
assert(oakengine_marker_get_color(detached) == 3);
|
||||
assert(oakengine_marker_get_name(detached, name, sizeof(name)) == 8);
|
||||
assert(strcmp(name, "Detached") == 0);
|
||||
assert(oakengine_marker_list_count(list) == 2);
|
||||
oakengine_marker_free(detached);
|
||||
|
||||
// Batch properties: recolor + rename both markers as ONE undo entry.
|
||||
OakEngineMarker *both[2] = { m0, m1 };
|
||||
assert(oakengine_marker_set_properties(both, 2, 7, "Same", 0, 0, 0, 0,
|
||||
0, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_get_color(m0) == 7);
|
||||
assert(oakengine_marker_get_color(m1) == 7);
|
||||
assert(oakengine_marker_get_name(m0, name, sizeof(name)) >= 0);
|
||||
assert(strcmp(name, "Same") == 0);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_get_color(m0) == 0);
|
||||
assert(oakengine_marker_get_color(m1) == 2);
|
||||
assert(oakengine_marker_get_name(m0, name, sizeof(name)) >= 0);
|
||||
assert(strcmp(name, "In") == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
|
||||
// Single-marker time move through the same batch call.
|
||||
assert(oakengine_marker_set_properties(both, 1, -1, NULL, 1, 10, 1, 12,
|
||||
1, NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_get_time(m0, &in_num, NULL, &out_num, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(in_num == 10 && out_num == 12);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
|
||||
// Remove with undo.
|
||||
assert(oakengine_marker_remove(m1) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_list_count(list) == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_list_count(list) == 2);
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_marker_get_time(NULL, &in_num, NULL, NULL, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_marker_get_name(NULL, name, sizeof(name)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_marker_get_color(NULL) == -1);
|
||||
assert(oakengine_marker_has_sibling_at_time(NULL, 1, 1) == 0);
|
||||
assert(oakengine_marker_set_time_live(NULL, 0, 1, 1, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_marker_list_add(NULL, 0, 1, 1, 1, "x", 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_marker_remove(NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_marker_set_properties(NULL, 1, 0, NULL, 0, 0, 0, 0, 0,
|
||||
NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// ---- Workarea handle family (B4c) ----------------------------------------------
|
||||
|
||||
static void test_workarea_handle_family(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Workarea");
|
||||
assert(seq != NULL);
|
||||
|
||||
// Reset sentinels: k_reset_in is 0, k_reset_out is RATIONAL_MAX.
|
||||
int64_t ri_num = 0, ri_den = 0, ro_num = 0, ro_den = 0;
|
||||
oakengine_workarea_reset_in_out(&ri_num, &ri_den, &ro_num, &ro_den);
|
||||
assert(ri_num == 0 && ri_den > 0);
|
||||
assert(ro_num > 0 && ro_den > 0);
|
||||
|
||||
OakEngineWorkarea *wa =
|
||||
oakengine_viewer_get_workarea_handle((OakEngineNode *)seq);
|
||||
assert(wa != NULL);
|
||||
assert(oakengine_viewer_get_workarea_handle(NULL) == NULL);
|
||||
|
||||
// A fresh workarea is disabled.
|
||||
int enabled = -1;
|
||||
assert(oakengine_workarea_get(wa, NULL, NULL, NULL, NULL, &enabled) ==
|
||||
OAKENGINE_OK);
|
||||
assert(enabled == 0);
|
||||
|
||||
// Undoable enable + range change.
|
||||
assert(oakengine_workarea_set_enabled_undoable(wa, 1, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_workarea_get(wa, NULL, NULL, NULL, NULL, &enabled) ==
|
||||
OAKENGINE_OK);
|
||||
assert(enabled == 1);
|
||||
assert(oakengine_workarea_set_range_undoable(wa, 1, 2, 3, 2, ri_num,
|
||||
ri_den, ro_num, ro_den,
|
||||
NULL) == OAKENGINE_OK);
|
||||
int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0;
|
||||
assert(oakengine_workarea_get(wa, &in_num, &in_den, &out_num, &out_den,
|
||||
NULL) == OAKENGINE_OK);
|
||||
assert(in_num == 1 && in_den == 2 && out_num == 3 && out_den == 2);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_workarea_get(wa, &in_num, NULL, &out_num, NULL,
|
||||
NULL) == OAKENGINE_OK);
|
||||
assert(in_num == ri_num && out_num == ro_num);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_workarea_get(wa, NULL, NULL, NULL, NULL, &enabled) ==
|
||||
OAKENGINE_OK);
|
||||
assert(enabled == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
|
||||
// Standalone workarea: enabled-undoable degrades to a direct apply
|
||||
// (no project owns it).
|
||||
OakEngineWorkarea *over = oakengine_workarea_create();
|
||||
assert(over != NULL);
|
||||
assert(oakengine_workarea_set_enabled_undoable(over, 1, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_workarea_get(over, NULL, NULL, NULL, NULL, &enabled) ==
|
||||
OAKENGINE_OK);
|
||||
assert(enabled == 1);
|
||||
assert(oakengine_workarea_set_range(over, 0, 1, 5, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_workarea_get(over, NULL, NULL, &out_num, &out_den,
|
||||
NULL) == OAKENGINE_OK);
|
||||
assert(out_num == 5 && out_den == 1);
|
||||
oakengine_workarea_free(over);
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_workarea_get(NULL, &in_num, NULL, NULL, NULL, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_workarea_set_range(NULL, 0, 1, 1, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_workarea_set_enabled(NULL, 1) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_workarea_set_range_undoable(NULL, 0, 1, 1, 1, 0, 1, 1,
|
||||
1, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_workarea_set_enabled_undoable(NULL, 1, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// ---- Clip input ids / media in / cache (B4c) -----------------------------------
|
||||
|
||||
static void test_clip_input_ids_and_media(void)
|
||||
{
|
||||
// Input id statics: non-null, distinct, and stable across calls.
|
||||
const char *ids[] = { oakengine_clip_buffer_input_id(),
|
||||
oakengine_clip_speed_input_id(),
|
||||
oakengine_clip_reverse_input_id(),
|
||||
oakengine_clip_maintain_audio_pitch_input_id(),
|
||||
oakengine_clip_loop_mode_input_id(),
|
||||
oakengine_clip_auto_cache_input_id() };
|
||||
for (size_t i = 0; i < sizeof(ids) / sizeof(ids[0]); i++) {
|
||||
assert(ids[i] != NULL && ids[i][0] != '\0');
|
||||
for (size_t j = i + 1; j < sizeof(ids) / sizeof(ids[0]); j++) {
|
||||
assert(strcmp(ids[i], ids[j]) != 0);
|
||||
}
|
||||
}
|
||||
assert(strcmp(oakengine_clip_speed_input_id(), ids[1]) == 0);
|
||||
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "ClipMedia");
|
||||
assert(seq != NULL);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
0);
|
||||
|
||||
char path[4096];
|
||||
demo_path(path, sizeof(path));
|
||||
OakEngineFootage *footage =
|
||||
oakengine_project_import_footage(project, path);
|
||||
assert(footage != NULL);
|
||||
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
|
||||
assert(clip != NULL);
|
||||
|
||||
// Media in-point: read via the range getter, write undoably.
|
||||
int64_t media_in = -1;
|
||||
assert(oakengine_clip_get_range(clip, NULL, NULL, &media_in) ==
|
||||
OAKENGINE_OK);
|
||||
assert(media_in == 0);
|
||||
assert(oakengine_clip_set_media_in(clip, 5, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_clip_get_range(clip, NULL, NULL, &media_in) ==
|
||||
OAKENGINE_OK);
|
||||
assert(media_in == 5);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_clip_get_range(clip, NULL, NULL, &media_in) ==
|
||||
OAKENGINE_OK);
|
||||
assert(media_in == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
|
||||
// Non-undoable mode applies directly.
|
||||
assert(oakengine_clip_set_media_in(clip, 2, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_clip_get_range(clip, NULL, NULL, &media_in) ==
|
||||
OAKENGINE_OK);
|
||||
assert(media_in == 2);
|
||||
|
||||
// Cache entry points: smoke calls (headless, no caches to speak of).
|
||||
oakengine_clip_request_invalidate(clip, 0, 0, 0);
|
||||
oakengine_clip_request_invalidate(clip, 1, 0, 30);
|
||||
oakengine_clip_add_cache_passthrough(clip, clip);
|
||||
oakengine_clip_discard_cache(clip);
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_clip_set_media_in(NULL, 0, 1) == OAKENGINE_E_INVALID);
|
||||
oakengine_clip_request_invalidate(NULL, 0, 0, 0);
|
||||
oakengine_clip_add_cache_passthrough(NULL, clip);
|
||||
oakengine_clip_add_cache_passthrough(clip, NULL);
|
||||
oakengine_clip_discard_cache(NULL);
|
||||
assert(oakengine_block_is_enabled(NULL) == 0);
|
||||
assert(oakengine_block_is_enabled((OakEngineBlock *)clip) == 1);
|
||||
|
||||
oakengine_footage_free(footage);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// ---- Track height helpers / default nodes (B4c) --------------------------------
|
||||
|
||||
static void test_track_height_helpers(void)
|
||||
{
|
||||
assert(oakengine_track_height_default() > 0.0);
|
||||
// Round-trip through the pixel conversion.
|
||||
const int px = oakengine_track_default_height_in_pixels();
|
||||
assert(px > 0);
|
||||
assert(oakengine_track_height_internal_to_pixels(
|
||||
oakengine_track_height_pixels_to_internal(px)) == px);
|
||||
assert(oakengine_track_height_internal_to_pixels(
|
||||
oakengine_track_height_default()) == px);
|
||||
}
|
||||
|
||||
static void test_add_default_nodes(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Defaults");
|
||||
assert(seq != NULL);
|
||||
|
||||
int video = -1, audio = -1;
|
||||
assert(oakengine_sequence_track_count(seq, &video, &audio, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(video == 0 && audio == 0);
|
||||
|
||||
// Adds one video + one audio track as ONE undo entry.
|
||||
assert(oakengine_sequence_add_default_nodes(seq) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_track_count(seq, &video, &audio, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(video == 1 && audio == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_track_count(seq, &video, &audio, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(video == 0 && audio == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_track_count(seq, &video, &audio, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(video == 1 && audio == 1);
|
||||
|
||||
assert(oakengine_sequence_add_default_nodes(NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// ---- clip_get_media_range_rational ------------------------------------------
|
||||
|
||||
static void test_clip_get_media_range_rational(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "MediaRange");
|
||||
assert(seq != NULL);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) == 0);
|
||||
|
||||
char path[4096];
|
||||
demo_path(path, sizeof(path));
|
||||
OakEngineFootage *footage =
|
||||
oakengine_project_import_footage(project, path);
|
||||
assert(footage != NULL);
|
||||
|
||||
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
|
||||
assert(clip != NULL);
|
||||
|
||||
// NULL handle.
|
||||
assert(oakengine_clip_get_media_range_rational(NULL, NULL, NULL, NULL,
|
||||
NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Valid clip: media range should have a non-zero duration.
|
||||
int64_t in_num = -1, in_den = -1, out_num = -1, out_den = -1;
|
||||
assert(oakengine_clip_get_media_range_rational(clip, &in_num, &in_den,
|
||||
&out_num, &out_den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(in_num >= 0 && in_den > 0 && out_num > in_num);
|
||||
|
||||
// Partial output pointers (any may be NULL).
|
||||
assert(oakengine_clip_get_media_range_rational(clip, NULL, &in_den,
|
||||
&out_num, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_clip_get_media_range_rational(clip, NULL, NULL, NULL,
|
||||
NULL) == OAKENGINE_OK);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// ---- clip_find_multicam / multicam_switch_source (basic) --------------------
|
||||
|
||||
static void test_multicam_basic(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
// clip_find_multicam: NULL clip returns NULL.
|
||||
assert(oakengine_clip_find_multicam(NULL) == NULL);
|
||||
|
||||
// A non-clip node (Solid) returns NULL.
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
assert(oakengine_clip_find_multicam(solid) == NULL);
|
||||
|
||||
// multicam_switch_source: NULL args.
|
||||
assert(oakengine_multicam_switch_source(NULL, NULL, 0, 0, 0.0, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
// ---- marker_list_add_existing -----------------------------------------------
|
||||
|
||||
static void test_marker_list_add_existing(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "MarkerAdopt");
|
||||
assert(seq != NULL);
|
||||
|
||||
OakEngineMarkerList *list =
|
||||
oakengine_viewer_get_marker_list((OakEngineNode *)seq);
|
||||
assert(list != NULL);
|
||||
|
||||
// Add a marker to the list, get its handle.
|
||||
assert(oakengine_marker_list_add(list, 0, 1, 2, 1, "Test", 0) ==
|
||||
OAKENGINE_OK);
|
||||
OakEngineMarker *marker = oakengine_marker_list_at(list, 0);
|
||||
assert(marker != NULL);
|
||||
|
||||
// Remove it from the list.
|
||||
assert(oakengine_marker_remove(marker) == OAKENGINE_OK);
|
||||
assert(oakengine_marker_list_count(list) == 0);
|
||||
|
||||
// add_existing to re-add it.
|
||||
assert(oakengine_marker_list_add_existing(list, marker) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_marker_list_count(list) == 1);
|
||||
|
||||
// NULL list or marker.
|
||||
assert(oakengine_marker_list_add_existing(NULL, marker) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_marker_list_add_existing(list, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
@@ -1237,6 +1828,16 @@ int main(void)
|
||||
test_batch_editing(path);
|
||||
test_batch_editing_round2(path);
|
||||
test_batch_editing_round3(path);
|
||||
test_sequence_clip();
|
||||
test_track_queries(path);
|
||||
test_marker_handle_family();
|
||||
test_workarea_handle_family();
|
||||
test_clip_input_ids_and_media();
|
||||
test_track_height_helpers();
|
||||
test_add_default_nodes();
|
||||
test_clip_get_media_range_rational();
|
||||
test_multicam_basic();
|
||||
test_marker_list_add_existing();
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/***
|
||||
|
||||
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 traverse facade
|
||||
// (oakengine/traverse.h) plus the node value-hint write path
|
||||
// (oakengine_node_set_value_hint()). Builds a small node graph with the
|
||||
// facade node family and exercises generate_database/generate_table, the db
|
||||
// accessors, element_index_for_hint, generate_row's C-side error paths and
|
||||
// transform. No GL required: evaluation is synchronous and CPU-only
|
||||
// (textures resolve as engine-side dummy textures), so no GL gating is
|
||||
// needed.
|
||||
|
||||
#include <assert.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/node.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/traverse.h"
|
||||
|
||||
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_traverse_test_%lu", base,
|
||||
(unsigned long)GetCurrentProcessId());
|
||||
assert(_mkdir(g_tmpdir) == 0);
|
||||
#else
|
||||
strcpy(g_tmpdir, "/tmp/oakengine_traverse_test_XXXXXX");
|
||||
assert(mkdtemp(g_tmpdir) != NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- Robustness: NULL/invalid arguments ------------------------------------
|
||||
|
||||
static void test_null_robustness(OakEngineNode *solid)
|
||||
{
|
||||
double m[6];
|
||||
|
||||
assert(oakengine_traverse_generate_database(NULL, 0, 1, 1, 1) == NULL);
|
||||
assert(oakengine_traverse_generate_table(NULL, 0, 1, 1, 1) == NULL);
|
||||
// Zero denominators are invalid rationals.
|
||||
assert(oakengine_traverse_generate_database(solid, 0, 0, 1, 1) == NULL);
|
||||
assert(oakengine_traverse_generate_table(solid, 0, 1, 1, 0) == NULL);
|
||||
|
||||
oakengine_traverse_db_free(NULL); // no-op
|
||||
|
||||
assert(oakengine_traverse_db_input_count(NULL) == 0);
|
||||
assert(oakengine_traverse_db_input_id(NULL, 0) == NULL);
|
||||
assert(oakengine_traverse_db_row_count(NULL, 0) == 0);
|
||||
assert(oakengine_traverse_row_type(NULL, 0, 0) == OAK_NODE_VALUE_NONE);
|
||||
assert(oakengine_traverse_row_source(NULL, 0, 0) == NULL);
|
||||
assert(oakengine_traverse_row_tag(NULL, 0, 0) != NULL); // never NULL
|
||||
assert(oakengine_traverse_row_value_string(NULL, 0, 0) == NULL);
|
||||
assert(oakengine_traverse_row_split_count(NULL, 0, 0) == 0);
|
||||
assert(oakengine_traverse_row_split_string(NULL, 0, 0, 0) == NULL);
|
||||
|
||||
assert(oakengine_traverse_table_element_index_for_hint(NULL, "x", -1,
|
||||
NULL) == -1);
|
||||
|
||||
// generate_row: the C side can only exercise the error paths -- the
|
||||
// real output is an olive::NodeValueRow (a C++ QHash typedef), which a
|
||||
// pure C test cannot allocate. The filled-row path is covered by the
|
||||
// application (the viewer display gizmo drag-start path).
|
||||
assert(oakengine_traverse_generate_row(NULL, 0, 1, 1, 1, NULL, 0, 0,
|
||||
(void *)1) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_traverse_generate_row(solid, 0, 1, 1, 1, NULL, 0, 0,
|
||||
NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_traverse_transform(NULL, solid, 0, 1, 1, 1, NULL, m) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_traverse_transform(solid, NULL, 0, 1, 1, 1, NULL, m) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_traverse_transform(solid, solid, 0, 1, 1, 1, NULL,
|
||||
NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
assert(oakengine_node_set_value_hint(NULL, "x", -1, OAK_NODE_VALUE_COLOR,
|
||||
0, NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_set_value_hint(solid, NULL, -1,
|
||||
OAK_NODE_VALUE_COLOR, 0,
|
||||
NULL) == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
// ---- generate_database + accessors ------------------------------------------
|
||||
|
||||
static void test_database(OakEngineNode *solid)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
OakEngineTraverseDb *db =
|
||||
oakengine_traverse_generate_database(solid, 0, 1, 1, 1);
|
||||
assert(db != NULL);
|
||||
|
||||
// One entry per node input, in the node's input order (deterministic).
|
||||
const int node_inputs = oakengine_node_input_count(solid);
|
||||
assert(node_inputs >= 2);
|
||||
assert(oakengine_traverse_db_input_count(db) == node_inputs);
|
||||
for (int i = 0; i < node_inputs; i++) {
|
||||
assert(oakengine_node_input_id(solid, i, buf, sizeof(buf)) > 0);
|
||||
const char *id = oakengine_traverse_db_input_id(db, i);
|
||||
assert(id != NULL);
|
||||
assert(strcmp(id, buf) == 0);
|
||||
|
||||
// Every input of an unconnected Solid generator produces one row
|
||||
// (its standard value).
|
||||
const int rows = oakengine_traverse_db_row_count(db, i);
|
||||
assert(rows >= 1);
|
||||
for (int r = 0; r < rows; r++) {
|
||||
// Type is a valid facade value type for plain inputs
|
||||
// (enabled_in is BOOL, color_in is COLOR).
|
||||
const int type = oakengine_traverse_row_type(db, i, r);
|
||||
assert(type > OAK_NODE_VALUE_NONE);
|
||||
// Source: the value's originating node, or NULL; the solid's
|
||||
// standard values are sourced from the node itself.
|
||||
OakEngineNode *src = oakengine_traverse_row_source(db, i, r);
|
||||
assert(src == NULL || src == solid);
|
||||
// Tag may be empty but never NULL.
|
||||
assert(oakengine_traverse_row_tag(db, i, r) != NULL);
|
||||
// Value string is non-empty for these value types.
|
||||
const char *vs = oakengine_traverse_row_value_string(db, i, r);
|
||||
assert(vs != NULL);
|
||||
assert(vs[0] != '\0');
|
||||
// Split values: at least one track, each with a string.
|
||||
const int splits = oakengine_traverse_row_split_count(db, i, r);
|
||||
assert(splits >= 1);
|
||||
for (int s = 0; s < splits; s++) {
|
||||
assert(oakengine_traverse_row_split_string(db, i, r, s) !=
|
||||
NULL);
|
||||
}
|
||||
assert(oakengine_traverse_row_split_string(db, i, r, splits) ==
|
||||
NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-range accessors fail cleanly.
|
||||
assert(oakengine_traverse_db_input_id(db, node_inputs) == NULL);
|
||||
assert(oakengine_traverse_db_row_count(db, node_inputs) == 0);
|
||||
assert(oakengine_traverse_row_type(db, node_inputs, 0) ==
|
||||
OAK_NODE_VALUE_NONE);
|
||||
|
||||
// A multi-entry database is not a generate_table result: the hint
|
||||
// lookup rejects it.
|
||||
assert(oakengine_traverse_table_element_index_for_hint(solid, "color_in",
|
||||
-1, db) == -1);
|
||||
|
||||
oakengine_traverse_db_free(db);
|
||||
}
|
||||
|
||||
// ---- generate_table + element_index_for_hint + set_value_hint -----------------
|
||||
|
||||
static void test_table_and_hints(OakEngineNode *solid, OakEngineNode *lut)
|
||||
{
|
||||
OakEngineTraverseDb *db =
|
||||
oakengine_traverse_generate_table(solid, 0, 1, 1, 1);
|
||||
assert(db != NULL);
|
||||
assert(oakengine_traverse_db_input_count(db) == 1);
|
||||
// The single output table is keyed by an empty input id.
|
||||
const char *id = oakengine_traverse_db_input_id(db, 0);
|
||||
assert(id != NULL);
|
||||
assert(id[0] == '\0');
|
||||
assert(oakengine_traverse_db_row_count(db, 0) >= 1);
|
||||
|
||||
// set_value_hint: unknown input ids and bogus types are rejected.
|
||||
assert(oakengine_node_set_value_hint(solid, "not_an_input", -1,
|
||||
OAK_NODE_VALUE_COLOR, 0,
|
||||
NULL) == OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_node_set_value_hint(solid, "color_in", -1, 999, 0,
|
||||
NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
// The solid's output table holds texture rows. A hint preferring COLOR
|
||||
// values (set on the lut's texture input) matches nothing -> -1.
|
||||
assert(oakengine_node_set_value_hint(lut, "tex_in", -1,
|
||||
OAK_NODE_VALUE_COLOR, -1,
|
||||
NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_traverse_table_element_index_for_hint(lut, "tex_in", -1,
|
||||
db) == -1);
|
||||
|
||||
// An untyped hint falls back to the input's declared type (k_texture
|
||||
// for "tex_in"), which does have a row in the table.
|
||||
assert(oakengine_node_set_value_hint(lut, "tex_in", -1,
|
||||
OAK_NODE_VALUE_NONE, -1,
|
||||
NULL) == OAKENGINE_OK);
|
||||
assert(oakengine_traverse_table_element_index_for_hint(lut, "tex_in", -1,
|
||||
db) >= 0);
|
||||
|
||||
oakengine_traverse_db_free(db);
|
||||
}
|
||||
|
||||
// ---- transform ------------------------------------------------------------------
|
||||
|
||||
static void test_transform(OakEngineNode *solid, OakEngineNode *lut)
|
||||
{
|
||||
double m[6] = { 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
// No transform-generating nodes between start and end: identity matrix.
|
||||
assert(oakengine_traverse_transform(solid, solid, 0, 1, 1, 1, NULL, m) ==
|
||||
OAKENGINE_OK);
|
||||
assert(m[0] == 1.0 && m[1] == 0.0 && m[2] == 0.0 && m[3] == 1.0);
|
||||
assert(m[4] == 0.0 && m[5] == 0.0);
|
||||
|
||||
// Same through an edge, with explicit cache params.
|
||||
oak_video_params vp;
|
||||
memset(&vp, 0, sizeof(vp));
|
||||
assert(oakengine_video_params_make(&vp, 1920, 1080, 1001, 30000, 0, 1, 1,
|
||||
0, 0, 1) == OAKENGINE_OK);
|
||||
memset(m, 0, sizeof(m));
|
||||
assert(oakengine_traverse_transform(solid, lut, 0, 1, 1, 1, &vp, m) ==
|
||||
OAKENGINE_OK);
|
||||
assert(m[0] == 1.0 && m[1] == 0.0 && m[2] == 0.0 && m[3] == 1.0);
|
||||
assert(m[4] == 0.0 && m[5] == 0.0);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
|
||||
// Sandbox the config/cache/data locations.
|
||||
#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
|
||||
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
OakEngineNode *lut = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.ociolut");
|
||||
assert(lut != NULL);
|
||||
|
||||
test_null_robustness(solid);
|
||||
test_database(solid);
|
||||
test_table_and_hints(solid, lut);
|
||||
test_transform(solid, lut);
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
printf("oakengine_traverse_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
/***
|
||||
|
||||
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 viewer facade (oakengine/viewer.h)
|
||||
// and the viewer events (oakengine/events.h ids 100-110). Exercises every
|
||||
// function of the family on a Sequence (a ViewerOutput subclass): handle
|
||||
// validation, input ids, playhead/length, stream params, enabled streams,
|
||||
// workarea, parameter setup, waveform and the change notifications. No GL
|
||||
// required (headless init, CPU only). Uses tests/demo.mp4 to give the
|
||||
// sequence real content length.
|
||||
|
||||
#include <assert.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/events.h"
|
||||
#include "oakengine/footage.h"
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/node.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/timeline.h"
|
||||
#include "oakengine/viewer.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_viewer_test_%lu", base,
|
||||
(unsigned long)GetCurrentProcessId());
|
||||
assert(_mkdir(g_tmpdir) == 0);
|
||||
#else
|
||||
strcpy(g_tmpdir, "/tmp/oakengine_viewer_test_XXXXXX");
|
||||
assert(mkdtemp(g_tmpdir) != NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void demo_path(char *dst, size_t cap)
|
||||
{
|
||||
const int n = snprintf(dst, cap, "%s/tests/demo.mp4", OAK_TEST_SOURCE_DIR);
|
||||
assert(n > 0 && (size_t)n < cap);
|
||||
}
|
||||
|
||||
// A sequence handle is the same engine object pointer as its node handle
|
||||
// (all facade handles are reinterpreted engine pointers; see the wrap()
|
||||
// helpers in src/capi/timeline.cpp).
|
||||
static OakEngineNode *as_node(OakEngineSequence *seq)
|
||||
{
|
||||
return (OakEngineNode *)seq;
|
||||
}
|
||||
|
||||
// ---- Handle validation / constants ----------------------------------------
|
||||
|
||||
static void test_from_node(OakEngineProject *project, OakEngineSequence *seq)
|
||||
{
|
||||
OakEngineNode *seq_node = as_node(seq);
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
|
||||
assert(oakengine_viewer_from_node(NULL) == NULL);
|
||||
assert(oakengine_viewer_from_node(solid) == NULL);
|
||||
assert(oakengine_viewer_from_node(seq_node) == seq_node);
|
||||
|
||||
assert(oakengine_viewer_from_const_node(NULL) == NULL);
|
||||
assert(oakengine_viewer_from_const_node((const OakEngineNode *)solid) ==
|
||||
NULL);
|
||||
assert(oakengine_viewer_from_const_node((const OakEngineNode *)seq_node) ==
|
||||
(const OakEngineNode *)seq_node);
|
||||
|
||||
// The input id constants are static, non-empty strings.
|
||||
assert(oakengine_viewer_video_params_input_id() != NULL);
|
||||
assert(oakengine_viewer_video_params_input_id()[0] != '\0');
|
||||
assert(oakengine_viewer_audio_params_input_id()[0] != '\0');
|
||||
assert(oakengine_viewer_subtitle_params_input_id()[0] != '\0');
|
||||
assert(oakengine_viewer_texture_input_id()[0] != '\0');
|
||||
assert(oakengine_viewer_samples_input_id()[0] != '\0');
|
||||
assert(oakengine_viewer_default_sample_format() >= 0);
|
||||
}
|
||||
|
||||
// ---- Playhead / length ------------------------------------------------------
|
||||
|
||||
static void test_playhead(OakEngineSequence *seq)
|
||||
{
|
||||
int64_t num = -1, den = -1;
|
||||
|
||||
assert(oakengine_viewer_get_playhead(NULL, &num, &den) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_playhead(as_node(seq), &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 0);
|
||||
|
||||
assert(oakengine_viewer_set_playhead(NULL, 1, 1) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_set_playhead(as_node(seq), 2, 1) == OAKENGINE_OK);
|
||||
num = den = -1;
|
||||
assert(oakengine_viewer_get_playhead(as_node(seq), &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 2 && den == 1);
|
||||
}
|
||||
|
||||
static void test_lengths(OakEngineSequence *seq)
|
||||
{
|
||||
int64_t num = -1, den = -1;
|
||||
|
||||
assert(oakengine_viewer_get_length(NULL, &num, &den) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_length(as_node(seq), &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 0);
|
||||
assert(oakengine_viewer_get_video_length(as_node(seq), &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 0);
|
||||
assert(oakengine_viewer_get_audio_length(as_node(seq), &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 0);
|
||||
}
|
||||
|
||||
// ---- Stream parameters --------------------------------------------------------
|
||||
|
||||
static void test_stream_params(OakEngineSequence *seq)
|
||||
{
|
||||
const OakEngineNode *node = (const OakEngineNode *)as_node(seq);
|
||||
oak_video_params vp;
|
||||
int sr = -1, format = -1;
|
||||
uint64_t layout = 1;
|
||||
|
||||
assert(oakengine_viewer_get_video_params(NULL, 0, &vp) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_video_params(node, 0, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// A fresh sequence has one video and one audio stream, no subtitles.
|
||||
assert(oakengine_viewer_get_video_stream_count(NULL) == 0);
|
||||
assert(oakengine_viewer_get_video_stream_count(node) == 1);
|
||||
assert(oakengine_viewer_get_audio_stream_count(node) == 1);
|
||||
assert(oakengine_viewer_get_subtitle_stream_count(node) == 0);
|
||||
|
||||
// In-range video params come from the sequence defaults.
|
||||
assert(oakengine_viewer_get_video_params(node, 0, &vp) == OAKENGINE_OK);
|
||||
assert(vp.width > 0 && vp.height > 0);
|
||||
assert(vp.time_base_num > 0 && vp.time_base_den > 0);
|
||||
|
||||
// Out-of-range yields a zeroed struct (documented in viewer.h).
|
||||
memset(&vp, 0xFF, sizeof(vp));
|
||||
assert(oakengine_viewer_get_video_params(node, 99, &vp) == OAKENGINE_OK);
|
||||
assert(vp.width == 0 && vp.height == 0);
|
||||
|
||||
// Audio params; out-of-range yields 0/0/0.
|
||||
assert(oakengine_viewer_get_audio_params(NULL, 0, &sr, &layout,
|
||||
&format) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_audio_params(node, 0, &sr, &layout,
|
||||
&format) == OAKENGINE_OK);
|
||||
assert(sr > 0 && layout != 0);
|
||||
sr = -1;
|
||||
layout = 1;
|
||||
format = -1;
|
||||
assert(oakengine_viewer_get_audio_params(node, 99, &sr, &layout,
|
||||
&format) == OAKENGINE_OK);
|
||||
assert(sr == 0 && layout == 0 && format == 0);
|
||||
|
||||
// Per-stream enabled flags: video/audio stream 0 are enabled by
|
||||
// default; subtitle has no stream 0.
|
||||
assert(oakengine_viewer_get_stream_enabled(NULL, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_stream_enabled(node, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 1);
|
||||
assert(oakengine_viewer_get_stream_enabled(node, OAKENGINE_TRACK_TYPE_AUDIO,
|
||||
0) == 1);
|
||||
assert(oakengine_viewer_get_stream_enabled(
|
||||
node, OAKENGINE_TRACK_TYPE_SUBTITLE, 0) == 0);
|
||||
assert(oakengine_viewer_get_stream_enabled(node, 99, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Subtitle access: no subtitle streams on a fresh sequence.
|
||||
assert(oakengine_viewer_get_subtitle_count(NULL, 0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_subtitle_count(node, 0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_subtitle_at(NULL, 0, 0) == NULL);
|
||||
assert(oakengine_viewer_get_subtitle_at(node, 0, 0) == NULL);
|
||||
}
|
||||
|
||||
static void test_enabled_streams(OakEngineSequence *seq)
|
||||
{
|
||||
const OakEngineNode *node = (const OakEngineNode *)as_node(seq);
|
||||
oak_video_params vp;
|
||||
|
||||
assert(oakengine_viewer_has_enabled_streams(NULL,
|
||||
OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
0);
|
||||
assert(oakengine_viewer_has_enabled_streams(node,
|
||||
OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
1);
|
||||
assert(oakengine_viewer_has_enabled_streams(node,
|
||||
OAKENGINE_TRACK_TYPE_AUDIO) ==
|
||||
1);
|
||||
assert(oakengine_viewer_has_enabled_streams(
|
||||
node, OAKENGINE_TRACK_TYPE_SUBTITLE) == 0);
|
||||
assert(oakengine_viewer_has_enabled_streams(node, 99) == 0);
|
||||
|
||||
assert(oakengine_viewer_get_first_enabled_video_stream(NULL, &vp) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_first_enabled_video_stream(node, &vp) ==
|
||||
OAKENGINE_OK);
|
||||
assert(vp.width > 0 && vp.height > 0);
|
||||
|
||||
// Enabled stream references: video:0 and audio:0.
|
||||
assert(oakengine_viewer_get_enabled_stream_count(NULL) == 0);
|
||||
const int count = oakengine_viewer_get_enabled_stream_count(node);
|
||||
assert(count == 2);
|
||||
// Query form (max = 0, NULL arrays) returns the total count.
|
||||
assert(oakengine_viewer_get_enabled_streams(node, NULL, NULL, 0) == count);
|
||||
|
||||
int types[8];
|
||||
int indices[8];
|
||||
memset(types, -1, sizeof(types));
|
||||
memset(indices, -1, sizeof(indices));
|
||||
assert(oakengine_viewer_get_enabled_streams(node, types, indices, 8) ==
|
||||
count);
|
||||
int saw_video = 0, saw_audio = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
assert(indices[i] == 0);
|
||||
if (types[i] == OAKENGINE_TRACK_TYPE_VIDEO) {
|
||||
saw_video = 1;
|
||||
} else if (types[i] == OAKENGINE_TRACK_TYPE_AUDIO) {
|
||||
saw_audio = 1;
|
||||
} else {
|
||||
assert(0); // unexpected stream type
|
||||
}
|
||||
}
|
||||
assert(saw_video && saw_audio);
|
||||
|
||||
// A smaller max truncates the write but still returns the total.
|
||||
types[0] = types[1] = -1;
|
||||
indices[0] = indices[1] = -1;
|
||||
assert(oakengine_viewer_get_enabled_streams(node, types, indices, 1) ==
|
||||
count);
|
||||
assert(types[0] != -1 && types[1] == -1);
|
||||
}
|
||||
|
||||
// ---- Workarea ------------------------------------------------------------------
|
||||
|
||||
static void test_workarea(OakEngineSequence *seq)
|
||||
{
|
||||
OakEngineNode *node = as_node(seq);
|
||||
oakengine_viewer_workarea wa;
|
||||
|
||||
assert(oakengine_viewer_get_workarea(NULL, &wa) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_get_workarea(node, NULL) == OAKENGINE_E_INVALID);
|
||||
memset(&wa, 0xFF, sizeof(wa));
|
||||
assert(oakengine_viewer_get_workarea(node, &wa) == OAKENGINE_OK);
|
||||
assert(wa.enabled == 0);
|
||||
|
||||
assert(oakengine_viewer_set_workarea_range(NULL, 0, 1, 1, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_set_workarea_range(node, 1, 1, 5, 1) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_viewer_set_workarea_enabled(NULL, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_set_workarea_enabled(node, 1) == OAKENGINE_OK);
|
||||
|
||||
memset(&wa, 0, sizeof(wa));
|
||||
assert(oakengine_viewer_get_workarea(node, &wa) == OAKENGINE_OK);
|
||||
assert(wa.in_num == 1 && wa.in_den == 1);
|
||||
assert(wa.out_num == 5 && wa.out_den == 1);
|
||||
assert(wa.enabled == 1);
|
||||
|
||||
assert(oakengine_viewer_set_workarea_enabled(node, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_viewer_get_workarea(node, &wa) == OAKENGINE_OK);
|
||||
assert(wa.enabled == 0);
|
||||
}
|
||||
|
||||
// ---- Parameter setup / waveform -------------------------------------------------
|
||||
|
||||
static void test_parameter_setup(OakEngineProject *project,
|
||||
OakEngineSequence *seq)
|
||||
{
|
||||
OakEngineNode *node = as_node(seq);
|
||||
|
||||
assert(oakengine_viewer_set_default_parameters(NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_set_default_parameters(node) == OAKENGINE_OK);
|
||||
|
||||
// set_parameters_from_footage accepts any viewer handles; a second
|
||||
// sequence stands in for the footage array here.
|
||||
OakEngineSequence *other = oakengine_sequence_new(project, "Other");
|
||||
assert(other != NULL);
|
||||
OakEngineNode *other_node = as_node(other);
|
||||
|
||||
assert(oakengine_viewer_set_parameters_from_footage(NULL, &other_node,
|
||||
1) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_set_parameters_from_footage(node, NULL, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
// An empty array is a valid no-op.
|
||||
assert(oakengine_viewer_set_parameters_from_footage(node, NULL, 0) ==
|
||||
OAKENGINE_OK);
|
||||
// One invalid element rejects the whole call.
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
OakEngineNode *mixed[2] = { other_node, solid };
|
||||
assert(oakengine_viewer_set_parameters_from_footage(node, mixed, 2) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
// All viewers: OK, and the params are adopted.
|
||||
OakEngineNode *viewers[1] = { other_node };
|
||||
assert(oakengine_viewer_set_parameters_from_footage(node, viewers, 1) ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
// Waveform toggle; nothing is connected to the samples input, so the
|
||||
// connected waveform is NULL.
|
||||
assert(oakengine_viewer_set_waveform_enabled(NULL, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_viewer_set_waveform_enabled(node, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_viewer_set_waveform_enabled(node, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_viewer_get_connected_waveform(NULL) == NULL);
|
||||
assert(oakengine_viewer_get_connected_waveform(
|
||||
(const OakEngineNode *)node) == NULL);
|
||||
}
|
||||
|
||||
// ---- Events ---------------------------------------------------------------------
|
||||
|
||||
struct EventLog {
|
||||
int playhead_events;
|
||||
int64_t playhead_num;
|
||||
int64_t playhead_den;
|
||||
int length_events;
|
||||
int64_t length_num;
|
||||
int64_t length_den;
|
||||
int size_events;
|
||||
int64_t size_w;
|
||||
int64_t size_h;
|
||||
int video_params_events;
|
||||
int audio_params_events;
|
||||
int sample_rate_events;
|
||||
int64_t sample_rate;
|
||||
int texture_events;
|
||||
int frame_rate_events;
|
||||
int pixel_aspect_events;
|
||||
int interlacing_events;
|
||||
int64_t interlacing_mode;
|
||||
int waveform_events;
|
||||
};
|
||||
|
||||
static void record_event(const oakengine_event *event, void *userdata)
|
||||
{
|
||||
struct EventLog *log = (struct EventLog *)userdata;
|
||||
assert(event != NULL);
|
||||
switch (event->id) {
|
||||
case OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED:
|
||||
log->playhead_events++;
|
||||
log->playhead_num = event->a;
|
||||
log->playhead_den = event->b;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED:
|
||||
log->length_events++;
|
||||
log->length_num = event->a;
|
||||
log->length_den = event->b;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_SIZE_CHANGED:
|
||||
log->size_events++;
|
||||
log->size_w = event->a;
|
||||
log->size_h = event->b;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED:
|
||||
log->video_params_events++;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED:
|
||||
log->audio_params_events++;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED:
|
||||
log->sample_rate_events++;
|
||||
log->sample_rate = event->a;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED:
|
||||
log->texture_events++;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED:
|
||||
log->frame_rate_events++;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED:
|
||||
log->pixel_aspect_events++;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED:
|
||||
log->interlacing_events++;
|
||||
log->interlacing_mode = event->a;
|
||||
break;
|
||||
case OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED:
|
||||
log->waveform_events++;
|
||||
break;
|
||||
default:
|
||||
assert(0); // unexpected event id on this subscription
|
||||
}
|
||||
}
|
||||
|
||||
static int64_t subscribe_checked(OakEngineNode *node, int32_t id,
|
||||
struct EventLog *log)
|
||||
{
|
||||
const int64_t sub = oakengine_event_subscribe(node, id, record_event, log);
|
||||
assert(sub > 0);
|
||||
return sub;
|
||||
}
|
||||
|
||||
static void test_events(OakEngineProject *project, OakEngineSequence *seq,
|
||||
const char *media_path)
|
||||
{
|
||||
struct EventLog log;
|
||||
memset(&log, 0, sizeof(log));
|
||||
OakEngineNode *node = as_node(seq);
|
||||
|
||||
// Family mismatch: a viewer event on a non-viewer node must fail.
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
assert(oakengine_event_subscribe(solid,
|
||||
OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED,
|
||||
record_event, &log) == 0);
|
||||
|
||||
int64_t subs[16];
|
||||
int n = 0;
|
||||
subs[n++] = subscribe_checked(node, OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED,
|
||||
&log);
|
||||
subs[n++] =
|
||||
subscribe_checked(node, OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED, &log);
|
||||
subs[n++] = subscribe_checked(
|
||||
node, OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED, &log);
|
||||
subs[n++] =
|
||||
subscribe_checked(node, OAKENGINE_EVENT_VIEWER_SIZE_CHANGED, &log);
|
||||
subs[n++] = subscribe_checked(
|
||||
node, OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED, &log);
|
||||
subs[n++] = subscribe_checked(
|
||||
node, OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED, &log);
|
||||
subs[n++] = subscribe_checked(
|
||||
node, OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED, &log);
|
||||
subs[n++] = subscribe_checked(
|
||||
node, OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED, &log);
|
||||
subs[n++] = subscribe_checked(
|
||||
node, OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED, &log);
|
||||
subs[n++] = subscribe_checked(
|
||||
node, OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED, &log);
|
||||
subs[n++] = subscribe_checked(
|
||||
node, OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED, &log);
|
||||
|
||||
// Playhead.
|
||||
assert(oakengine_viewer_set_playhead(node, 3, 1) == OAKENGINE_OK);
|
||||
assert(log.playhead_events == 1);
|
||||
assert(log.playhead_num == 3 && log.playhead_den == 1);
|
||||
|
||||
// Length: placing a real clip makes verify_length() emit
|
||||
// length_changed with the new content length.
|
||||
OakEngineFootage *footage =
|
||||
oakengine_project_import_footage(project, media_path);
|
||||
assert(footage != NULL);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) == 0);
|
||||
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
|
||||
assert(clip != NULL);
|
||||
assert(log.length_events >= 1);
|
||||
assert(log.length_num > 0 && log.length_den > 0);
|
||||
int64_t len_num = -1, len_den = -1;
|
||||
assert(oakengine_viewer_get_length(node, &len_num, &len_den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(len_num == log.length_num && len_den == log.length_den);
|
||||
|
||||
// Video params: changing the size emits size_changed (with the new
|
||||
// dimensions as a/b) and video_params_changed.
|
||||
assert(oakengine_sequence_set_video_params(seq, 1280, 720, -1, -1, -1, -1,
|
||||
-1, -1, 0) == OAKENGINE_OK);
|
||||
assert(log.size_events == 1);
|
||||
assert(log.size_w == 1280 && log.size_h == 720);
|
||||
assert(log.video_params_events == 1);
|
||||
assert(log.frame_rate_events == 0);
|
||||
assert(log.pixel_aspect_events == 0);
|
||||
assert(log.interlacing_events == 0);
|
||||
|
||||
// Pixel aspect and interlacing changes fire their own events.
|
||||
assert(oakengine_sequence_set_video_params(seq, -1, -1, -1, -1, 4, 3, -1,
|
||||
-1, 0) == OAKENGINE_OK);
|
||||
assert(log.pixel_aspect_events == 1);
|
||||
assert(oakengine_sequence_set_video_params(seq, -1, -1, -1, -1, -1, -1, 1,
|
||||
-1, 0) == OAKENGINE_OK);
|
||||
assert(log.interlacing_events == 1);
|
||||
assert(log.interlacing_mode == 1);
|
||||
|
||||
// Audio params: a new sample rate emits sample_rate_changed (a = rate)
|
||||
// and audio_params_changed.
|
||||
assert(oakengine_sequence_set_audio_params(seq, 44100, 0, 0) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.sample_rate_events == 1);
|
||||
assert(log.sample_rate == 44100);
|
||||
assert(log.audio_params_events == 1);
|
||||
|
||||
// Texture input: the placed clip's track auto-connected the viewer's
|
||||
// texture input, so disconnect it first, then connect a node and check
|
||||
// that texture_input_changed fired.
|
||||
assert(oakengine_node_disconnect(node,
|
||||
oakengine_viewer_texture_input_id()) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_node_connect(solid, node,
|
||||
oakengine_viewer_texture_input_id()) ==
|
||||
OAKENGINE_OK);
|
||||
assert(log.texture_events >= 1);
|
||||
assert(oakengine_node_disconnect(node,
|
||||
oakengine_viewer_texture_input_id()) ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
// connected_waveform_changed requires a connected sample output with a
|
||||
// waveform cache; there is no audio-producing node chain in this test,
|
||||
// so only the subscription itself is exercised above.
|
||||
|
||||
while (n > 0) {
|
||||
assert(oakengine_event_unsubscribe(subs[--n]) == OAKENGINE_OK);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
|
||||
// Sandbox the config/cache/data locations.
|
||||
#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
|
||||
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "ViewerSeq");
|
||||
assert(seq != NULL);
|
||||
|
||||
test_from_node(project, seq);
|
||||
test_playhead(seq);
|
||||
test_lengths(seq);
|
||||
test_stream_params(seq);
|
||||
test_enabled_streams(seq);
|
||||
test_workarea(seq);
|
||||
test_parameter_setup(project, seq);
|
||||
|
||||
char media[4096];
|
||||
demo_path(media, sizeof(media));
|
||||
|
||||
// test_parameter_setup() called set_default_parameters(), which reads
|
||||
// the (empty, sandboxed) user config and may leave invalid params
|
||||
// behind (same hazard oakengine_sequence_new() backfills against).
|
||||
// Restore known-good params so the clip/timebase paths work.
|
||||
assert(oakengine_sequence_set_video_params(seq, 1920, 1080, 30000, 1001,
|
||||
1, 1, 0, -1, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_set_audio_params(seq, 48000, 3, 0) ==
|
||||
OAKENGINE_OK);
|
||||
|
||||
test_events(project, seq, media);
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
printf("oakengine_viewer_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/***
|
||||
|
||||
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 render-worker facade
|
||||
// (oakengine/worker.h). Exercises the session state machine without a
|
||||
// renderer ("none" backend): create/destroy, malformed and unknown control
|
||||
// messages, handshake validation, message ordering errors and shutdown
|
||||
// idempotency. No GPU and no QApplication required: every path exercised
|
||||
// here is a validation/error path that never touches a render backend.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "oakengine/worker.h"
|
||||
|
||||
// Handle one line into a heap buffer sized via the buf/size query
|
||||
// convention. Returns the response (empty string when the message has no
|
||||
// reply); the caller frees it. Asserts the query/fill round-trip agrees.
|
||||
static char *handle(OakWorkerSession *session, const char *line)
|
||||
{
|
||||
const int needed =
|
||||
oakengine_worker_session_handle_json(session, line, NULL, 0);
|
||||
assert(needed >= 0);
|
||||
char *buf = static_cast<char *>(malloc(size_t(needed) + 1));
|
||||
const int written = oakengine_worker_session_handle_json(
|
||||
session, line, buf, needed + 1);
|
||||
assert(written == needed);
|
||||
buf[needed] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void assert_is_error_with(const char *response, const char *needle)
|
||||
{
|
||||
if (!strstr(response, "\"type\":\"error\"") ||
|
||||
!strstr(response, needle)) {
|
||||
fprintf(stderr,
|
||||
"expected error response containing \"%s\", got: %s\n",
|
||||
needle, response);
|
||||
assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_create_destroy(void)
|
||||
{
|
||||
// NULL, "" and "none" all skip renderer creation
|
||||
const char *backends[] = { NULL, "", "none", "NONE" };
|
||||
for (size_t i = 0; i < sizeof(backends) / sizeof(backends[0]); ++i) {
|
||||
OakWorkerSession *s = oakengine_worker_session_create(backends[i]);
|
||||
assert(s);
|
||||
assert(oakengine_worker_session_has_renderer(s) == 0);
|
||||
assert(oakengine_worker_session_shutdown_requested(s) == 0);
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
// NULL tolerance
|
||||
oakengine_worker_session_free(NULL);
|
||||
assert(oakengine_worker_session_has_renderer(NULL) == 0);
|
||||
assert(oakengine_worker_session_shutdown_requested(NULL) == 0);
|
||||
assert(oakengine_worker_session_handle_json(NULL, "{}", NULL, 0) == -1);
|
||||
}
|
||||
|
||||
static void test_startup_handshake(void)
|
||||
{
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
assert(s);
|
||||
|
||||
// buf/size query convention
|
||||
const int needed =
|
||||
oakengine_worker_session_startup_handshake(s, NULL, 0);
|
||||
assert(needed > 0);
|
||||
char *buf = static_cast<char *>(malloc(size_t(needed) + 1));
|
||||
assert(oakengine_worker_session_startup_handshake(s, buf, needed + 1) ==
|
||||
needed);
|
||||
buf[needed] = '\0';
|
||||
assert(strstr(buf, "\"type\":\"handshake\""));
|
||||
assert(strstr(buf, "\"protocol_version\":1"));
|
||||
// No renderer -> no GL version announced
|
||||
assert(!strstr(buf, "gl_major"));
|
||||
free(buf);
|
||||
|
||||
assert(oakengine_worker_session_startup_handshake(NULL, NULL, 0) == -1);
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
static void test_initialize_runtime(void)
|
||||
{
|
||||
// NULL tolerance
|
||||
assert(oakengine_worker_session_initialize_runtime(NULL) == 0);
|
||||
|
||||
// Runtime init (EngineCore, factories, managers) must succeed without a
|
||||
// renderer; the session stays usable for control messages afterwards.
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
assert(s);
|
||||
assert(oakengine_worker_session_initialize_runtime(s) == 1);
|
||||
char *r = handle(s, "{\"type\":\"teleport\"}");
|
||||
assert_is_error_with(r, "unknown message type");
|
||||
free(r);
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
static void test_malformed_json(void)
|
||||
{
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
char *r = handle(s, "{not json at all");
|
||||
assert_is_error_with(r, "malformed control message");
|
||||
free(r);
|
||||
r = handle(s, "[1,2,3]");
|
||||
assert_is_error_with(r, "malformed control message");
|
||||
free(r);
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
static void test_unknown_type(void)
|
||||
{
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
char *r = handle(s, "{\"type\":\"teleport\"}");
|
||||
assert_is_error_with(r, "unknown message type: teleport");
|
||||
free(r);
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
static void test_handshake_validation(void)
|
||||
{
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
|
||||
// Protocol version mismatch is rejected before any shm access
|
||||
char *r = handle(s,
|
||||
"{\"type\":\"handshake\",\"protocol_version\":999,"
|
||||
"\"shm_key\":\"x\",\"output_slots\":1,"
|
||||
"\"slot_data_bytes\":16}");
|
||||
assert_is_error_with(r, "unsupported protocol version 999");
|
||||
free(r);
|
||||
|
||||
// Matching version but no shared-memory geometry
|
||||
r = handle(s, "{\"type\":\"handshake\",\"protocol_version\":1}");
|
||||
assert_is_error_with(r, "missing output shared-memory geometry");
|
||||
free(r);
|
||||
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
static void test_render_frame_before_load_graph(void)
|
||||
{
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
char *r = handle(s,
|
||||
"{\"type\":\"render_frame\",\"ticket\":7,"
|
||||
"\"node_uuid\":\"abc\",\"time_num\":0,\"time_den\":1}");
|
||||
assert_is_error_with(r, "render_frame received before load_graph");
|
||||
// The error carries the ticket id so the caller can correlate
|
||||
assert(strstr(r, "\"ticket\":7"));
|
||||
free(r);
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
static void test_load_graph_missing_file(void)
|
||||
{
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
char *r = handle(s,
|
||||
"{\"type\":\"load_graph\","
|
||||
"\"path\":\"/nonexistent/definitely/missing.ove\"}");
|
||||
assert_is_error_with(r, "graph file does not exist");
|
||||
free(r);
|
||||
// A failed load must not arm the session: render_frame still complains
|
||||
// about the missing graph, not about the shm handshake order
|
||||
r = handle(s,
|
||||
"{\"type\":\"render_frame\",\"ticket\":1,"
|
||||
"\"node_uuid\":\"abc\",\"time_num\":0,\"time_den\":1}");
|
||||
assert_is_error_with(r, "render_frame received before load_graph");
|
||||
free(r);
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
static void test_shutdown_idempotent(void)
|
||||
{
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
|
||||
// Shutdown produces no response and latches the flag
|
||||
char *r = handle(s, "{\"type\":\"shutdown\"}");
|
||||
assert(r[0] == '\0');
|
||||
free(r);
|
||||
assert(oakengine_worker_session_shutdown_requested(s) == 1);
|
||||
|
||||
// Repeating it is a harmless no-op
|
||||
r = handle(s, "{\"type\":\"shutdown\"}");
|
||||
assert(r[0] == '\0');
|
||||
free(r);
|
||||
assert(oakengine_worker_session_shutdown_requested(s) == 1);
|
||||
|
||||
// The session still answers other messages afterwards
|
||||
r = handle(s, "{\"type\":\"teleport\"}");
|
||||
assert_is_error_with(r, "unknown message type");
|
||||
free(r);
|
||||
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
static void test_cancel_is_silent(void)
|
||||
{
|
||||
OakWorkerSession *s = oakengine_worker_session_create("none");
|
||||
char *r = handle(s, "{\"type\":\"cancel\",\"ticket\":3}");
|
||||
assert(r[0] == '\0');
|
||||
free(r);
|
||||
oakengine_worker_session_free(s);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_create_destroy();
|
||||
test_startup_handshake();
|
||||
test_initialize_runtime();
|
||||
test_malformed_json();
|
||||
test_unknown_type();
|
||||
test_handshake_validation();
|
||||
test_render_frame_before_load_graph();
|
||||
test_load_graph_missing_file();
|
||||
test_shutdown_idempotent();
|
||||
test_cancel_is_silent();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user