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,
|
int clip_index,
|
||||||
int64_t new_in);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -780,4 +780,288 @@ int oakengine_sequence_move_clip(OakEngineSequence *seq, int track_type,
|
|||||||
return OAKENGINE_OK;
|
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"
|
} // 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.
|
// 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)
|
int main(void)
|
||||||
{
|
{
|
||||||
make_tmpdir();
|
make_tmpdir();
|
||||||
@@ -470,6 +667,8 @@ int main(void)
|
|||||||
test_add_clip(project, seq, path);
|
test_add_clip(project, seq, path);
|
||||||
test_cross_project_rejected(path);
|
test_cross_project_rejected(path);
|
||||||
test_edit_round2(path);
|
test_edit_round2(path);
|
||||||
|
test_track_structure(path);
|
||||||
|
test_markers();
|
||||||
|
|
||||||
oakengine_project_free(project);
|
oakengine_project_free(project);
|
||||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||||
|
|||||||
Reference in New Issue
Block a user