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

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