engine: add sequence structure to the timeline facade

- undoable track removal (content restored on undo) and true-move
  track reordering assembled from the edge commands (the engine has
  no move-track API)
- track height/mute/lock getters and setters, documented as
  non-undoable to match the engine's current semantics
- undoable marker add/remove/rename; duplicate timestamps are
  rejected with E_STATE instead of hitting the engine's debug assert
This commit is contained in:
2026-07-20 08:42:03 +08:00
parent eaa301b755
commit 71b1243ad8
3 changed files with 587 additions and 0 deletions
+104
View File
@@ -343,6 +343,110 @@ OAKENGINE_API int oakengine_sequence_move_clip(OakEngineSequence *seq,
int clip_index,
int64_t new_in);
/* ---- Track structure and markers ------------------------------------------
*
* Track structure edits are undoable like the other editing primitives.
* Track height, mute and lock are NOT undoable in the engine (the
* application wires those buttons straight to the setters, see
* app/widget/timelinewidget/trackview/trackviewitem.cpp), and this family
* follows that behavior. Marker edits are undoable.
* All marker times are frame timestamps in the sequence's frame-rate
* timebase, like the rest of the family.
*/
/**
* @brief Remove a track and its content (undoable;
* olive::TimelineRemoveTrackCommand). OAKENGINE_E_NOT_FOUND when the track
* does not exist.
*/
OAKENGINE_API int oakengine_sequence_remove_track(OakEngineSequence *seq,
int track_type,
int track_index);
/**
* @brief Move the track at `from_index` to `to_index` within the track
* list (undoable).
*
* Implemented as a true move (take out and re-insert, the tracks in
* between shift), assembled from the engine's edge commands. `from_index`
* == `to_index` is a no-op success; an out-of-range index fails with
* OAKENGINE_E_NOT_FOUND.
*/
OAKENGINE_API int oakengine_sequence_move_track(OakEngineSequence *seq,
int track_type,
int from_index, int to_index);
/**
* @brief Track height in the engine's internal units
* (Track::get_track_height(); NOT undoable, see the section comment).
*/
OAKENGINE_API int oakengine_track_get_height(const OakEngineSequence *seq,
int track_type, int track_index,
double *height);
/**
* @brief Set the track height in internal units
* (Track::set_track_height(); NOT undoable). `height` must be > 0.
*/
OAKENGINE_API int oakengine_track_set_height(OakEngineSequence *seq,
int track_type, int track_index,
double height);
/**
* @brief 1 if the track is muted (Track::is_muted()).
*/
OAKENGINE_API int oakengine_track_is_muted(const OakEngineSequence *seq,
int track_type, int track_index);
/**
* @brief Mute or unmute the track (Track::set_muted(); NOT undoable).
*/
OAKENGINE_API int oakengine_track_set_muted(OakEngineSequence *seq,
int track_type, int track_index,
int muted);
/**
* @brief 1 if the track is locked (Track::is_locked()).
*/
OAKENGINE_API int oakengine_track_is_locked(const OakEngineSequence *seq,
int track_type, int track_index);
/**
* @brief Lock or unlock the track (Track::set_locked(); NOT undoable).
*/
OAKENGINE_API int oakengine_track_set_locked(OakEngineSequence *seq,
int track_type, int track_index,
int locked);
/**
* @brief Add a timeline marker at `time_ts` named `name` (undoable;
* olive::MarkerAddCommand).
*
* The engine does not allow two markers at the exact same time (its
* insertion asserts on it), so adding one fails with OAKENGINE_E_STATE.
* `name` may be NULL for an empty name.
*/
OAKENGINE_API int oakengine_sequence_marker_add(OakEngineSequence *seq,
int64_t time_ts,
const char *name);
/**
* @brief Remove the (first) marker at `time_ts` (undoable;
* olive::MarkerRemoveCommand). OAKENGINE_E_NOT_FOUND when no marker exists
* at that exact time.
*/
OAKENGINE_API int oakengine_sequence_marker_remove(OakEngineSequence *seq,
int64_t time_ts);
/**
* @brief Rename the (first) marker at `time_ts` (undoable;
* olive::MarkerChangeNameCommand). OAKENGINE_E_NOT_FOUND when no marker
* exists at that exact time.
*/
OAKENGINE_API int oakengine_sequence_marker_rename(OakEngineSequence *seq,
int64_t time_ts,
const char *name);
#ifdef __cplusplus
}
#endif
+284
View File
@@ -780,4 +780,288 @@ int oakengine_sequence_move_clip(OakEngineSequence *seq, int track_type,
return OAKENGINE_OK;
}
/* ---- Track structure and markers ------------------------------------------ */
int oakengine_sequence_remove_track(OakEngineSequence *seq, int track_type,
int track_index)
{
set_seq_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
set_seq_error(QStringLiteral("invalid sequence or track type"));
return OAKENGINE_E_INVALID;
}
olive::TrackList *list =
sequence->track_list(to_track_type(track_type));
if (track_index < 0 || track_index >= list->get_track_count()) {
set_seq_error(QStringLiteral("no track at index %1")
.arg(track_index));
return OAKENGINE_E_NOT_FOUND;
}
push_or_run(new olive::TimelineRemoveTrackCommand(
list->get_track_at(track_index)),
QStringLiteral("Remove Track"));
return OAKENGINE_OK;
}
int oakengine_sequence_move_track(OakEngineSequence *seq, int track_type,
int from_index, int to_index)
{
set_seq_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
set_seq_error(QStringLiteral("invalid sequence or track type"));
return OAKENGINE_E_INVALID;
}
olive::TrackList *list =
sequence->track_list(to_track_type(track_type));
const int count = list->get_track_count();
if (from_index < 0 || from_index >= count || to_index < 0 ||
to_index >= count) {
set_seq_error(QStringLiteral("track index out of range (%1 tracks)")
.arg(count));
return OAKENGINE_E_NOT_FOUND;
}
if (from_index == to_index) {
return OAKENGINE_OK;
}
// True move: compute the reordered track list, then rewire every track's
// array-element connection into that order with the engine's edge
// commands (one undoable command, so undo restores the old order).
QVector<olive::Track *> order = list->get_tracks();
olive::Track *moved = order.at(from_index);
order.removeAt(from_index);
order.insert(to_index, moved);
// Array elements stay put (connections just move between them); collect
// them in slot order.
QVector<int> elements;
for (int i = 0; i < count; i++) {
elements.append(list->get_array_index_from_cache_index(i));
}
const QString input_id = list->track_input();
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
for (int i = 0; i < count; i++) {
command->add_child(new olive::NodeEdgeRemoveCommand(
list->get_tracks().at(i),
olive::NodeInput(sequence, input_id, elements.at(i))));
}
for (int i = 0; i < count; i++) {
command->add_child(new olive::NodeEdgeAddCommand(
order.at(i),
olive::NodeInput(sequence, input_id, elements.at(i))));
}
push_or_run(command, QStringLiteral("Move Track"));
return OAKENGINE_OK;
}
int oakengine_track_get_height(const OakEngineSequence *seq, int track_type,
int track_index, double *height)
{
set_seq_error(QString());
const olive::Sequence *sequence =
reinterpret_cast<const olive::Sequence *>(seq);
if (!sequence || !height || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
set_seq_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::TrackList *list =
sequence->track_list(to_track_type(track_type));
if (track_index < 0 || track_index >= list->get_track_count()) {
set_seq_error(QStringLiteral("no track at index %1")
.arg(track_index));
return OAKENGINE_E_NOT_FOUND;
}
*height = list->get_track_at(track_index)->get_track_height();
return OAKENGINE_OK;
}
int oakengine_track_set_height(OakEngineSequence *seq, int track_type,
int track_index, double height)
{
set_seq_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
if (!sequence || height <= 0.0 ||
track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
set_seq_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::TrackList *list =
sequence->track_list(to_track_type(track_type));
if (track_index < 0 || track_index >= list->get_track_count()) {
set_seq_error(QStringLiteral("no track at index %1")
.arg(track_index));
return OAKENGINE_E_NOT_FOUND;
}
// Straight setter like the application (not undoable).
list->get_track_at(track_index)->set_track_height(height);
return OAKENGINE_OK;
}
int oakengine_track_is_muted(const OakEngineSequence *seq, int track_type,
int track_index)
{
const olive::Sequence *sequence =
reinterpret_cast<const olive::Sequence *>(seq);
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
return 0;
}
olive::TrackList *list =
sequence->track_list(to_track_type(track_type));
if (track_index < 0 || track_index >= list->get_track_count()) {
return 0;
}
return list->get_track_at(track_index)->is_muted() ? 1 : 0;
}
int oakengine_track_set_muted(OakEngineSequence *seq, int track_type,
int track_index, int muted)
{
set_seq_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
set_seq_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::TrackList *list =
sequence->track_list(to_track_type(track_type));
if (track_index < 0 || track_index >= list->get_track_count()) {
set_seq_error(QStringLiteral("no track at index %1")
.arg(track_index));
return OAKENGINE_E_NOT_FOUND;
}
list->get_track_at(track_index)->set_muted(muted != 0);
return OAKENGINE_OK;
}
int oakengine_track_is_locked(const OakEngineSequence *seq, int track_type,
int track_index)
{
const olive::Sequence *sequence =
reinterpret_cast<const olive::Sequence *>(seq);
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
return 0;
}
olive::TrackList *list =
sequence->track_list(to_track_type(track_type));
if (track_index < 0 || track_index >= list->get_track_count()) {
return 0;
}
return list->get_track_at(track_index)->is_locked() ? 1 : 0;
}
int oakengine_track_set_locked(OakEngineSequence *seq, int track_type,
int track_index, int locked)
{
set_seq_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
set_seq_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::TrackList *list =
sequence->track_list(to_track_type(track_type));
if (track_index < 0 || track_index >= list->get_track_count()) {
set_seq_error(QStringLiteral("no track at index %1")
.arg(track_index));
return OAKENGINE_E_NOT_FOUND;
}
list->get_track_at(track_index)->set_locked(locked != 0);
return OAKENGINE_OK;
}
int oakengine_sequence_marker_add(OakEngineSequence *seq, int64_t time_ts,
const char *name)
{
set_seq_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
if (!sequence) {
set_seq_error(QStringLiteral("invalid sequence"));
return OAKENGINE_E_INVALID;
}
olive::Rational tb;
if (!time_base_of(sequence, &tb)) {
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
return OAKENGINE_E_STATE;
}
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
olive::TimelineMarkerList *markers = sequence->get_markers();
if (markers->get_marker_at_time(time)) {
// The engine's marker insertion asserts on duplicate times.
set_seq_error(QStringLiteral("a marker already exists at time %1")
.arg(time_ts));
return OAKENGINE_E_STATE;
}
push_or_run(new olive::MarkerAddCommand(
markers, olive::TimeRange(time, time),
QString::fromUtf8(name ? name : ""), 0),
QStringLiteral("Add Marker"));
return OAKENGINE_OK;
}
int oakengine_sequence_marker_remove(OakEngineSequence *seq, int64_t time_ts)
{
set_seq_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
if (!sequence) {
set_seq_error(QStringLiteral("invalid sequence"));
return OAKENGINE_E_INVALID;
}
olive::Rational tb;
if (!time_base_of(sequence, &tb)) {
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
return OAKENGINE_E_STATE;
}
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
olive::TimelineMarker *marker =
sequence->get_markers()->get_marker_at_time(time);
if (!marker) {
set_seq_error(QStringLiteral("no marker at time %1").arg(time_ts));
return OAKENGINE_E_NOT_FOUND;
}
push_or_run(new olive::MarkerRemoveCommand(marker),
QStringLiteral("Remove Marker"));
return OAKENGINE_OK;
}
int oakengine_sequence_marker_rename(OakEngineSequence *seq, int64_t time_ts,
const char *name)
{
set_seq_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
if (!sequence) {
set_seq_error(QStringLiteral("invalid sequence"));
return OAKENGINE_E_INVALID;
}
olive::Rational tb;
if (!time_base_of(sequence, &tb)) {
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
return OAKENGINE_E_STATE;
}
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
olive::TimelineMarker *marker =
sequence->get_markers()->get_marker_at_time(time);
if (!marker) {
set_seq_error(QStringLiteral("no marker at time %1").arg(time_ts));
return OAKENGINE_E_NOT_FOUND;
}
push_or_run(new olive::MarkerChangeNameCommand(
marker, QString::fromUtf8(name ? name : "")),
QStringLiteral("Rename Marker"));
return OAKENGINE_OK;
}
} // extern "C"
@@ -444,6 +444,203 @@ static void test_edit_round2(const char *media_path)
// Footage imported into one project must not be placed into another.
// Track structure: move/height/mute/lock/remove over two video tracks,
// plus undo/redo of the undoable ones.
static void test_track_structure(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, "Tracks");
assert(seq != NULL);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
0);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
1);
OakEngineFootage *footage =
oakengine_project_import_footage(project, media_path);
assert(footage != NULL);
// Distinctive clips per track so track order is observable.
OakEngineClip *clip_a = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0);
OakEngineClip *clip_b = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 1, 20, 30, 0);
assert(clip_a != NULL && clip_b != NULL);
int64_t in = -1, out = -1, media_in = -1;
double height = 0.0;
// move_track swaps their positions; the clips move with the tracks.
assert(oakengine_sequence_move_track(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
1) == OAKENGINE_OK);
OakEngineClip *now = oakengine_sequence_clip_at(
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
assert(now == clip_b);
assert(oakengine_clip_get_range(now, &in, &out, &media_in) ==
OAKENGINE_OK);
assert(in == 20 && out == 30);
now = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 1, 0);
assert(now == clip_a);
assert(oakengine_clip_get_range(now, &in, &out, &media_in) ==
OAKENGINE_OK);
assert(in == 0 && out == 10);
// Undo/redo restore the order both ways.
assert(oakengine_project_undo(project) == OAKENGINE_OK);
now = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
assert(now == clip_a);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
now = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
assert(now == clip_b);
// No-op move and out-of-range indexes.
assert(oakengine_sequence_move_track(seq, OAKENGINE_TRACK_TYPE_VIDEO, 1,
1) == OAKENGINE_OK);
assert(oakengine_sequence_move_track(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
5) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_sequence_move_track(seq, OAKENGINE_TRACK_TYPE_VIDEO, 5,
0) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_sequence_move_track(NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0,
1) == OAKENGINE_E_INVALID);
// Height: default is positive, round-trip, direct (not undoable).
assert(oakengine_track_get_height(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
&height) == OAKENGINE_OK);
const double default_height = height;
assert(default_height > 0.0);
assert(oakengine_track_set_height(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
3.0) == OAKENGINE_OK);
assert(oakengine_track_get_height(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
&height) == OAKENGINE_OK);
assert(height == 3.0);
assert(oakengine_track_set_height(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
0.0) == OAKENGINE_E_INVALID);
assert(oakengine_track_get_height(seq, OAKENGINE_TRACK_TYPE_VIDEO, 99,
&height) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_track_get_height(NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0,
&height) == OAKENGINE_E_INVALID);
// Mute and lock toggles, direct (not undoable).
assert(oakengine_track_is_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0) == 0);
assert(oakengine_track_set_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
1) == OAKENGINE_OK);
assert(oakengine_track_is_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0) == 1);
assert(oakengine_track_set_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
0) == OAKENGINE_OK);
assert(oakengine_track_is_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0) == 0);
assert(oakengine_track_set_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 99,
1) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_track_is_locked(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0) ==
0);
assert(oakengine_track_set_locked(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
1) == OAKENGINE_OK);
assert(oakengine_track_is_locked(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0) ==
1);
assert(oakengine_track_set_locked(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
0) == OAKENGINE_OK);
assert(oakengine_track_set_locked(NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0,
1) == OAKENGINE_E_INVALID);
// remove_track drops the track and its content; undo brings both back.
int video = -1, audio = -1, subtitle = -1;
assert(oakengine_sequence_remove_track(seq, OAKENGINE_TRACK_TYPE_VIDEO,
1) == OAKENGINE_OK);
assert(oakengine_sequence_track_count(seq, &video, &audio, &subtitle) ==
OAKENGINE_OK);
assert(video == 1);
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
0) == 1);
assert(oakengine_sequence_remove_track(seq, OAKENGINE_TRACK_TYPE_VIDEO,
9) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_sequence_track_count(seq, &video, &audio, &subtitle) ==
OAKENGINE_OK);
assert(video == 2);
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
1) == 1);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_sequence_track_count(seq, &video, &audio, &subtitle) ==
OAKENGINE_OK);
assert(video == 1);
oakengine_footage_free(footage);
oakengine_project_free(project);
}
static void test_markers(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "Markers");
assert(seq != NULL);
int64_t ts = -1;
char name[64];
assert(oakengine_sequence_marker_count(seq) == 0);
assert(oakengine_sequence_marker_add(seq, 30, "Chapter 1") ==
OAKENGINE_OK);
assert(oakengine_sequence_marker_count(seq) == 1);
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name)) ==
OAKENGINE_OK);
assert(ts == 30 && strcmp(name, "Chapter 1") == 0);
// Duplicate time: the engine forbids two markers at one time.
assert(oakengine_sequence_marker_add(seq, 30, "dup") ==
OAKENGINE_E_STATE);
// Earlier marker sorts in front.
assert(oakengine_sequence_marker_add(seq, 10, "Intro") == OAKENGINE_OK);
assert(oakengine_sequence_marker_count(seq) == 2);
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name)) ==
OAKENGINE_OK);
assert(ts == 10 && strcmp(name, "Intro") == 0);
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
OAKENGINE_OK);
assert(ts == 30 && strcmp(name, "Chapter 1") == 0);
assert(oakengine_sequence_marker_at(seq, 2, &ts, name, sizeof(name)) ==
OAKENGINE_E_NOT_FOUND);
// Rename with undo/redo.
assert(oakengine_sequence_marker_rename(seq, 30, "Chapter 2") ==
OAKENGINE_OK);
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
OAKENGINE_OK);
assert(strcmp(name, "Chapter 2") == 0);
assert(oakengine_sequence_marker_rename(seq, 77, "nope") ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
OAKENGINE_OK);
assert(strcmp(name, "Chapter 1") == 0);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
OAKENGINE_OK);
assert(strcmp(name, "Chapter 2") == 0);
// Remove with undo; removing again reports not found.
assert(oakengine_sequence_marker_remove(seq, 10) == OAKENGINE_OK);
assert(oakengine_sequence_marker_count(seq) == 1);
assert(oakengine_sequence_marker_remove(seq, 10) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_sequence_marker_count(seq) == 2);
// NULL safety.
assert(oakengine_sequence_marker_add(NULL, 0, "x") ==
OAKENGINE_E_INVALID);
assert(oakengine_sequence_marker_remove(NULL, 0) ==
OAKENGINE_E_INVALID);
assert(oakengine_sequence_marker_rename(NULL, 0, "x") ==
OAKENGINE_E_INVALID);
assert(oakengine_sequence_remove_track(NULL, OAKENGINE_TRACK_TYPE_VIDEO,
0) == OAKENGINE_E_INVALID);
oakengine_project_free(project);
}
int main(void)
{
make_tmpdir();
@@ -470,6 +667,8 @@ int main(void)
test_add_clip(project, seq, path);
test_cross_project_rejected(path);
test_edit_round2(path);
test_track_structure(path);
test_markers();
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);