/*** 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 . ***/ // 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 #include #include #include #if defined(_WIN32) #include #include #else #include #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; }