tellers-timeline-core 0.1.0

An OpenTimelineIO timeline engine with policy-driven editing, sync, sanitization, and effect conventions.
Documentation
use tellers_timeline_core::track_methods::track_item_insert::{InsertPolicy, OverlapPolicy};
use std::collections::HashMap;
use tellers_timeline_core::*;

fn make_clip_with_id(duration: Seconds, id: &str) -> Item {
    let mut refs: HashMap<String, MediaReference> = HashMap::new();
    refs.insert(
        "DEFAULT_MEDIA".to_string(),
        MediaReference::ExternalReference {
            target_url: "mem://".to_string(),
            available_range: Some(TimeRange {
                otio_schema: "TimeRange.1".to_string(),
                duration: RationalTime { otio_schema: "RationalTime.1".to_string(), rate: 1.0, value: duration },
                start_time: RationalTime { otio_schema: "RationalTime.1".to_string(), rate: 1.0, value: 0.0 },
            }),
            name: None,
            available_image_bounds: None,
            metadata: serde_json::Value::Null,
        },
    );
    let mut it = Item::Clip(Clip {
        otio_schema: "Clip.2".to_string(),
        enabled: true,
        name: Some("c".to_string()),
        source_range: TimeRange { otio_schema: "TimeRange.1".to_string(), duration: RationalTime { otio_schema: "RationalTime.1".to_string(), rate: 1.0, value: duration }, start_time: RationalTime { otio_schema: "RationalTime.1".to_string(), rate: 1.0, value: 0.0 } },
        media_references: refs,
        active_media_reference_key: Some("DEFAULT_MEDIA".to_string()),
        metadata: serde_json::Value::Null,
        effects: Vec::new(),
        markers: Vec::new(),
    });
    it.set_id(Some(id.to_string()));
    it
}

#[test]
fn stack_get_and_remove_item() {
    let mut tl = Timeline::default();
    let mut t1 = Track::default();
    let mut t2 = Track::default();
    let id1 = "id-1";
    let id2 = "id-2";

    t1.items.push(make_clip_with_id(2.0, id1));
    t2.items.push(make_clip_with_id(3.0, id2));

    // assign track ids
    let tid1 = "tid-1".to_string();
    let tid2 = "tid-2".to_string();
    t1.set_id(Some(tid1.clone()));
    t2.set_id(Some(tid2.clone()));

    tl.tracks.children.push(t1);
    tl.tracks.children.push(t2);

    // get item by id
    let got = tl.tracks.get_item(id1).expect("should find");
    assert_eq!(got.0, 0);
    assert_eq!(got.1, 0);

    // remove without gap
    let removed = tl.tracks.delete_item(id1, false, true);
    assert_eq!(removed.len(), 1);
    assert_eq!(removed[0].0, 0);
    assert!(matches!(removed[0].1, Item::Clip(_)));
    assert!(tl.tracks.children[0].items.is_empty());
}

#[test]
fn stack_move_item_between_tracks_at_time() {
    let mut tl = Timeline::default();
    let mut t1 = Track::default();
    let mut t2 = Track::default();

    // assign track ids
    let tid1 = "tid-1".to_string();
    let tid2 = "tid-2".to_string();
    t1.set_id(Some(tid1.clone()));
    t2.set_id(Some(tid2.clone()));

    // put one clip in t1 and a gap clip in t2
    let id_move = "id-move";
    t1.items.push(make_clip_with_id(2.0, id_move));
    t2.items.push(Item::Gap(Gap::make_gap(1.0)));

    tl.tracks.children.push(t1);
    tl.tracks.children.push(t2);

    // move to track 2 at time 1.0
    let ok = tl.tracks.move_item_at_time(
        id_move,
        &tid2,
        1.0,
        false,
        InsertPolicy::InsertBeforeOrAfter,
        OverlapPolicy::Override,
    );
    assert!(ok);

    // source track is now empty
    assert!(tl.tracks.children[0].items.is_empty());

    // dest track now contains two items, and the moved clip is present
    let dest = &tl.tracks.children[1];
    assert_eq!(dest.items.len(), 2);
    let found = dest.items.iter().any(|it| it.get_id().as_deref() == Some(id_move));
    assert!(found);
}

#[test]
fn moving_a_clip_preserves_an_adjacent_gap_id() {
    let mut source = Track::new(TrackKind::Video, Some("source".to_string()));
    source.items.push(make_clip_with_id(2.0, "moving"));
    let mut trailing_gap = Item::Gap(Gap::make_gap(3.0));
    trailing_gap.set_id(Some("trailing-gap".to_string()));
    source.items.push(trailing_gap);
    source.items.push(make_clip_with_id(1.0, "remaining"));

    let destination = Track::new(TrackKind::Video, Some("destination".to_string()));
    let mut stack = Stack::default();
    stack.children = vec![source, destination];

    assert!(stack.move_item_at_time(
        "moving",
        "destination",
        0.0,
        true,
        InsertPolicy::SplitAndInsert,
        OverlapPolicy::Override,
    ));

    let (gap_track, gap_index, _) = stack.get_item("trailing-gap").expect("gap ID is stable");
    assert_eq!(gap_track, 0);
    assert_eq!(stack.children[gap_track].items[gap_index].duration(), 5.0);
    assert!(stack.get_item("moving").is_some());
}