Editing a timeline
The ten edit operations, what each one does to its neighbours, and which of them change a track's length.
Appending clips to a track builds a cut. Changing one is a different problem, because every change has a question attached: what happens to everything after it?
There are ten operations, and they are the ones a non-linear editor has. They are functions rather than methods, because each is about two objects and belongs to neither — which is how upstream arranges them too.
use opentime::{RationalTime, TimeRange};
use otio_core::schema::{Base, Clip, ItemData, Track};
use otio_core::{Document, Node, NodeId};
/// One second of picture, named.
fn second(document: &mut Document, name: &str) -> NodeId {
document.insert(Node::Clip(Clip {
item: ItemData {
base: Base {
name: name.into(),
..Base::default()
},
source_range: Some(TimeRange::new(
RationalTime::new(0.0, 24.0),
RationalTime::new(24.0, 24.0),
)),
..ItemData::new()
},
..Clip::default()
}))
}
fn show(document: &Document, track: NodeId) -> Result<(), otio_core::Error> {
let names: Vec<&str> = document
.children_of(track)?
.iter()
.map(|id| document.try_get(*id).map(Node::name).unwrap_or(""))
.collect();
println!("{} - {} frames", names.join(" "), document.duration(track)?.value());
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut document = Document::new();
let track = document.insert(Node::Track(Track {
kind: "Video".into(),
..Track::default()
}));
for name in ["A", "B", "C"] {
let clip = second(&mut document, name);
document.append_child(track, clip)?;
}
show(&document, track)?;
// Insert makes room: everything from the insertion point onwards moves
// later, and the track gets longer.
let inserted = second(&mut document, "D");
otio_core::edit::insert(
&mut document,
inserted,
track,
RationalTime::new(24.0, 24.0),
false,
None,
)?;
show(&document, track)?;
// Overwrite does not: it lays an item over a span and whatever was in
// that span gives way. The track is the same length afterwards.
let laid = second(&mut document, "E");
otio_core::edit::overwrite(
&mut document,
laid,
track,
TimeRange::new(RationalTime::new(48.0, 24.0), RationalTime::new(24.0, 24.0)),
false,
None,
)?;
show(&document, track)?;
Ok(())
}import opentimelineio as otio
def second(name):
"""One second of picture, named."""
return otio.schema.Clip(
name=name,
source_range=otio.opentime.TimeRange(
otio.opentime.RationalTime(0, 24), otio.opentime.RationalTime(24, 24)
),
)
def show(track):
names = " ".join(child.name for child in track)
print(names, "-", track.duration().value, "frames")
track = otio.schema.Track(name="V1", kind="Video")
for name in ["A", "B", "C"]:
track.append(second(name))
show(track)
# Insert makes room: everything from the insertion point onwards moves later,
# and the track gets longer.
otio.algorithms.insert(
second("D"), track, otio.opentime.RationalTime(24, 24), remove_transitions=False
)
show(track)
# Overwrite does not: it lays an item over a span and whatever was in that
# span gives way. The track is the same length afterwards.
otio.algorithms.overwrite(
second("E"),
track,
otio.opentime.TimeRange(
otio.opentime.RationalTime(48, 24), otio.opentime.RationalTime(24, 24)
),
remove_transitions=False,
)
show(track)import { init, Clip, edit, RationalTime, TimeRange, Track } from "@alchemist-edit/otio";
await init();
/** One second of picture, named. */
function second(name: string): Clip {
const clip = new Clip({ name });
clip.sourceRange = new TimeRange(
new RationalTime(0, 24),
new RationalTime(24, 24),
);
return clip;
}
function show(track: Track) {
const names = track.children().map((child) => child.name).join(" ");
console.log(names, "-", track.duration().value, "frames");
}
const track = new Track({ name: "V1", kind: "Video" });
for (const name of ["A", "B", "C"]) track.appendChild(second(name));
show(track);
// Insert makes room: everything from the insertion point onwards moves later,
// and the track gets longer.
edit.insert(second("D"), track, new RationalTime(24, 24), false);
show(track);
// Overwrite does not: it lays an item over a span and whatever was in that
// span gives way. The track is the same length afterwards.
edit.overwrite(
second("E"),
track,
new TimeRange(new RationalTime(48, 24), new RationalTime(24, 24)),
false,
);
show(track);package main
import (
"fmt"
"log"
"strings"
otio "github.com/alchemist-editor/otio-rust/sdk/go"
)
// second makes one second of picture, named.
func second(name string) otio.Clip {
clip, err := otio.NewClip(name)
if err != nil {
log.Fatal(err)
}
if err := clip.SetSourceRange(otio.TimeRange{
StartTime: otio.RationalTime{Value: 0, Rate: 24},
Duration: otio.RationalTime{Value: 24, Rate: 24},
}); err != nil {
log.Fatal(err)
}
return clip
}
func show(track otio.Track) {
children, err := track.Children()
if err != nil {
log.Fatal(err)
}
names := make([]string, 0, len(children))
for _, child := range children {
name, _ := child.Name()
names = append(names, name)
}
duration, _ := track.Duration()
fmt.Printf("%s - %v frames\n", strings.Join(names, " "), duration.Value)
}
func main() {
track, err := otio.NewTrack("V1", "Video")
if err != nil {
log.Fatal(err)
}
for _, name := range []string{"A", "B", "C"} {
if err := track.AppendChild(second(name).Node); err != nil {
log.Fatal(err)
}
}
show(track)
// Insert makes room: everything from the insertion point onwards moves
// later, and the track gets longer.
at := otio.RationalTime{Value: 24, Rate: 24}
if err := otio.Insert(second("D").Node, track.Node, at, false, nil); err != nil {
log.Fatal(err)
}
show(track)
// Overwrite does not: it lays an item over a span and whatever was in
// that span gives way. The track is the same length afterwards.
over := otio.TimeRange{
StartTime: otio.RationalTime{Value: 48, Rate: 24},
Duration: otio.RationalTime{Value: 24, Rate: 24},
}
if err := otio.Overwrite(second("E").Node, track.Node, over, false, nil); err != nil {
log.Fatal(err)
}
show(track)
}import OpenTimelineIO
/// One second of picture, named.
func second(_ name: String) throws -> Clip {
let clip = try Clip(name: name)
try clip.setSourceRange(
TimeRange(
startTime: RationalTime(value: 0, rate: 24),
duration: RationalTime(value: 24, rate: 24)))
return clip
}
func show(_ track: Track) throws {
let names = try track.children().map { try $0.name() }.joined(separator: " ")
print(names, "-", try track.duration().value, "frames")
}
let track = try Track(name: "V1", kind: "Video")
for name in ["A", "B", "C"] {
try track.appendChild(try second(name))
}
try show(track)
// Insert makes room: everything from the insertion point onwards moves later,
// and the track gets longer.
try OTIO.insert(
try second("D"), composition: track,
time: RationalTime(value: 24, rate: 24), removeTransitions: false)
try show(track)
// Overwrite does not: it lays an item over a span and whatever was in that
// span gives way. The track is the same length afterwards.
try OTIO.overwrite(
try second("E"), composition: track,
range: TimeRange(
startTime: RationalTime(value: 48, rate: 24),
duration: RationalTime(value: 24, rate: 24)),
removeTransitions: false)
try show(track) <iostream>
<string>
<opentimelineio/otio.hpp>
/// One second of picture, named.
static otio::Clip second(const std::string &name) {
otio::Clip clip = otio::Clip::create(name);
clip.set_source_range(
otio::TimeRange(otio::RationalTime(0, 24), otio::RationalTime(24, 24)));
return clip;
}
static void show(const otio::Track &track) {
std::string names;
for (const otio::SerializableObject &child : track.children()) {
if (!names.empty()) names += " ";
names += child.name();
}
std::cout << names << " - " << track.duration().value << " frames\n";
}
int main() {
otio::Track track = otio::Track::create("V1", "Video");
for (const char *name : {"A", "B", "C"}) {
track.append_child(second(name));
}
show(track);
// Insert makes room: everything from the insertion point onwards moves
// later, and the track gets longer.
otio::insert(second("D"), track, otio::RationalTime(24, 24), false);
show(track);
// Overwrite does not: it lays an item over a span and whatever was in
// that span gives way. The track is the same length afterwards.
otio::overwrite(
second("E"), track,
otio::TimeRange(otio::RationalTime(48, 24), otio::RationalTime(24, 24)),
false);
show(track);
return 0;
}const std = ("std");
const otio = ("otio");
/// One second of picture, named.
fn second(document: *otio.Document, name: [:0]const u8) !otio.Clip {
const clip = try otio.Clip.init(document, name);
try clip.setSourceRange(.{
.start_time = .{ .value = 0, .rate = 24 },
.duration = .{ .value = 24, .rate = 24 },
});
return clip;
}
fn show(allocator: std.mem.Allocator, track: otio.Track) !void {
// Anything the library hands back that has to be freed is copied into an
// allocator you pass, and freed with `allocator.free`.
const children = try track.children(allocator);
defer allocator.free(children);
for (children, 0..) |child, index| {
const name = try child.name(allocator);
defer allocator.free(name);
if (index > 0) std.debug.print(" ", .{});
std.debug.print("{s}", .{name});
}
const duration = try track.duration();
std.debug.print(" - {d} frames\n", .{duration.value});
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const document = try otio.Document.init();
defer document.deinit();
const track = try otio.Track.init(document, "V1", "Video");
for ([_][:0]const u8{ "A", "B", "C" }) |name| {
try track.appendChild((try second(document, name)).asNode());
}
try show(allocator, track);
// Insert makes room: everything from the insertion point onwards moves
// later, and the track gets longer.
const inserted = try second(document, "D");
try document.insert(inserted.asNode(), track.asNode(), .{ .value = 24, .rate = 24 }, false, null);
try show(allocator, track);
// Overwrite does not: it lays an item over a span and whatever was in
// that span gives way. The track is the same length afterwards.
const laid = try second(document, "E");
try document.overwrite(laid.asNode(), track.asNode(), .{
.start_time = .{ .value = 48, .rate = 24 },
.duration = .{ .value = 24, .rate = 24 },
}, false, null);
try show(allocator, track);
}
/* One second of picture, named. */
static OtioNode second(OtioDocument *document, const char *name) {
OtioNode clip;
otio_clip_new(document, name, &clip, NULL);
OtioTimeRange span;
span.start_time.value = 0.0;
span.start_time.rate = 24.0;
span.duration.value = 24.0;
span.duration.rate = 24.0;
otio_item_set_source_range(document, clip, span, NULL);
return clip;
}
static void show(OtioDocument *document, OtioNode track) {
size_t count = 0;
otio_node_children(document, track, NULL, 0, &count, NULL);
OtioNode *children = malloc(count * sizeof *children);
otio_node_children(document, track, children, count, &count, NULL);
for (size_t index = 0; index < count; index += 1) {
OtioBuffer name;
if (otio_node_name(document, children[index], &name, NULL) == OTIO_STATUS_OK) {
printf("%s%s", index == 0 ? "" : " ", name.data);
otio_buffer_free(name);
}
}
free(children);
OtioRationalTime duration;
otio_item_duration(document, track, &duration, NULL);
printf(" - %g frames\n", duration.value);
}
int main(void) {
OtioDocument *document = otio_document_new();
OtioNode track;
otio_track_new(document, "V1", "Video", &track, NULL);
const char *names[] = {"A", "B", "C"};
for (size_t index = 0; index < 3; index += 1) {
otio_composition_append_child(document, track, second(document, names[index]), NULL);
}
show(document, track);
/* Insert makes room: everything from the insertion point onwards moves
* later, and the track gets longer. */
OtioRationalTime at = {24.0, 24.0};
otio_edit_insert(document, second(document, "D"), track, at, false, otio_node_none(), NULL);
show(document, track);
/* Overwrite does not: it lays an item over a span and whatever was in
* that span gives way. The track is the same length afterwards. */
OtioTimeRange over;
over.start_time.value = 48.0;
over.start_time.rate = 24.0;
over.duration.value = 24.0;
over.duration.rate = 24.0;
otio_edit_overwrite(document, second(document, "E"), track, over, false, otio_node_none(), NULL);
show(document, track);
otio_document_free(document);
return 0;
}using System;
using System.Linq;
using OpenTimelineIO;
// One second of picture, named.
static Clip Second(string name)
{
var clip = new Clip(name);
clip.SetSourceRange(new TimeRange(new RationalTime(0, 24), new RationalTime(24, 24)));
return clip;
}
static void Show(Track track)
{
var names = string.Join(" ", track.Children().Select(child => child.Name()));
Console.WriteLine($"{names} - {track.Duration().Value} frames");
}
var track = new Track("V1", "Video");
foreach (var name in new[] { "A", "B", "C" })
{
track.AppendChild(Second(name));
}
Show(track);
// Insert makes room: everything from the insertion point onwards moves later,
// and the track gets longer.
Otio.Insert(Second("D"), track, new RationalTime(24, 24), false, null);
Show(track);
// Overwrite does not: it lays an item over a span and whatever was in that
// span gives way. The track is the same length afterwards.
Otio.Overwrite(
Second("E"),
track,
new TimeRange(new RationalTime(48, 24), new RationalTime(24, 24)),
false,
null);
Show(track);
/// One second of picture, named.
static OTIOClip *Second(NSString *name) {
NSError *error = nil;
OTIOClip *clip = [OTIOClip clipWithName:name error:&error];
[clip setSourceRange:OTIOTimeRangeMake(OTIORationalTimeMake(0, 24),
OTIORationalTimeMake(24, 24))
error:&error];
return clip;
}
static void Show(OTIOTrack *track) {
NSError *error = nil;
NSMutableArray<NSString *> *names = [NSMutableArray array];
for (OTIOSerializableObject *child in [track children:&error]) {
[names addObject:[child name:&error]];
}
OTIORationalTime duration;
[track getDuration:&duration error:&error];
NSLog(@"%@ - %g frames", [names componentsJoinedByString:@" "], duration.value);
}
int main(void) {
{
NSError *error = nil;
OTIOTrack *track = [OTIOTrack trackWithName:@"V1" kind:@"Video" error:&error];
for (NSString *name in @[ @"A", @"B", @"C" ]) {
[track appendChild:Second(name) error:&error];
}
Show(track);
// Insert makes room: everything from the insertion point onwards
// moves later, and the track gets longer.
OTIOInsert(Second(@"D"), track, OTIORationalTimeMake(24, 24), NO, nil, &error);
Show(track);
// Overwrite does not: it lays an item over a span and whatever was
// in that span gives way. The track is the same length afterwards.
OTIOOverwrite(Second(@"E"), track,
OTIOTimeRangeMake(OTIORationalTimeMake(48, 24),
OTIORationalTimeMake(24, 24)),
NO, nil, &error);
Show(track);
}
return 0;
}The ten
| Operation | What it does |
|---|---|
insert | Puts an item at an instant, pushing what follows later. |
overwrite | Lays an item over a span, replacing what was there. |
trim | Moves an item's in and out points without moving its neighbours. |
ripple | Moves an item's in and out points, sliding everything after it. |
roll | Moves the cut between an item and its neighbour. |
slip | Moves the media inside an item without moving the item. |
slide | Moves an item along its track, taking the time from its neighbours. |
slice | Cuts whatever sits at an instant into two. |
fill | Drops an item into a gap, fitted as the reference point says. |
remove | Takes whatever sits at an instant out. |
The question each one answers
The useful way to hold these is by what they do to the track's length and to the neighbours.
Length changes. insert makes the track longer by the length of what
went in. remove without a fill makes it shorter, because what follows moves
up; remove with a fill leaves a gap and the length alone. ripple changes
it by however much the in or out point moved.
Length does not change. overwrite replaces rather than displaces.
trim, roll, slip and slide all move boundaries around within the
track without altering how much track there is. slice puts a cut in and
changes nothing else — two items where there was one, occupying the same
span.
Neighbours move. insert, ripple and remove without a fill. Those
are the three that shift material you did not name.
Neighbours do not move. overwrite, trim, slip, slice and fill.
roll and slide are the interesting pair: both change a neighbour, but
only by moving the boundary they share, so nothing further along the track
notices.
The sample above is insert and overwrite side by side, because that is
the distinction people reach for first and the one that decides whether a cut
gets longer.
Filling what an edit opens
Several of these can open a hole — overwrite past the end of a track,
remove with a fill, insert at a point past the end.
Each takes a fill template: the item to put in that hole. Pass nothing
and it is an ordinary Gap, which is transparent, so on a multi-track
timeline whatever is underneath shows through. When you want something else —
solid black, a slate, colour bars — pass a clip with a GeneratorReference
as the template and that is what fills it instead.
Slip and slide
These two are named after the editing desk and the names are worth learning the way round.
slip keeps the item where it is on the track and moves the media inside
it: same four seconds of the cut, a different four seconds of the shot. The
track does not change at all, and neither does any neighbour.
slide keeps the media and moves the item along the track: the same four
seconds of the shot, arriving earlier or later. The time has to come from
somewhere, so the neighbours on either side absorb it — the one before grows
or shrinks, and the one after does the opposite.
Where they are, and where they are not
All ten are in the core, in the C ABI, in every SDK generated from it, and in
Python as opentimelineio.algorithms.overwrite, insert and the rest, with a
ReferencePoint enum for fill's four-point edits.
Upstream's own Python package does not bind them; only its C++ has them. The
Python functions here follow that C++ editAlgorithm.h in their names,
parameters and defaults, and raise what its error handler raises. An object an
edit takes out of a track stays usable while Python holds it, with no parent,
as any removed child does.
What the copied piece holds
slice, insert and overwrite copy the piece of an item a split leaves
over, and fill with ReferencePoint.Sequence copies the clip it drops in.
The copy is made as upstream makes it, by the equivalent of writing the item
out and reading it back, so it shares nothing with the original: an object
the item holds in two places — one object under two metadata keys, one effect
listed twice, one media reference under two keys — becomes two separate
objects in the copy. The item left in place keeps what it held.
An item that holds itself
Metadata can hold whole objects, including the item it belongs to:
clip.metadata["self"] = clip. slice, insert and overwrite copy the
piece of an item a split leaves over, and fill copies the clip it drops in;
here that copy keeps the cycle, so the leftover piece holds itself just as
the original does, and the edit goes through like any other.
That is a deliberate difference from upstream. Upstream copies by writing the
item out and reading it back, which cannot carry a cycle, so the same edits
fail there with a cycle error — and slice, insert and overwrite fail
only after changing the track, leaving the clip cut short and the rest of it
lost. The cycle still cannot be saved: writing it as .otio is refused, here
as upstream.