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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user