# The data model

> Timelines, stacks, tracks, clips, gaps, transitions — and the handles that name them.

An OTIO document is a tree of objects that occupy time. There are not many
kinds, and the names are the ones upstream uses.

```text
Timeline
└── Stack  ("tracks")
    ├── Track  ("V1", kind "Video")
    │   ├── Clip   ("shot_01")
    │   ├── Gap
    │   └── Clip   ("shot_02")
    └── Track  ("A1", kind "Audio")
        └── Clip   ("dialogue")
```

A **Timeline** is the whole cut. It holds one **Stack**, conventionally
called `tracks`, and a stack lays its children over the same span of time —
which is what a track being "above" another means. A **Track** is a strand:
its children run one after another, and a **Gap** is the hole where nothing
plays. A **Clip** is a piece of media with a range taken out of it, and a
**Transition** is the overlap between two of them.

## Time, and what a range means

Every duration in OTIO is a `RationalTime`: a value and a rate, kept apart.
24 frames at 24fps is not the number 1 — it is `24/24`, and the rate travels
with it. This is the single most consequential decision in the format,
because it is what stops a 23.976 sequence from drifting into a 24 one by
accident.

Rust:

```rust
use opentime::RationalTime;

fn main() -> Result<(), opentime::TimeError> {
    // A time is a value and a rate, not a number of seconds. Four seconds at
    // 24 is 96 units; the rate travels with it so nothing has to guess later.
    let start = RationalTime::from_timecode("01:00:00:00", 24.0)?;
    let duration = RationalTime::from_frames(96.0, 24.0);

    let end = start + duration;
    println!("{} for {} seconds", end.to_timecode()?, duration.to_seconds());

    // Comparison rescales first, so the same instant at two rates is equal.
    assert_eq!(RationalTime::new(24.0, 24.0), RationalTime::new(48.0, 48.0));

    Ok(())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/rust.rs</sub>

Python:

```python
import opentimelineio as otio

# A time is a value and a rate, not a number of seconds. Four seconds at 24 is
# 96 units; the rate travels with it so nothing has to guess later.
start = otio.opentime.RationalTime.from_timecode("01:00:00:00", 24)
duration = otio.opentime.RationalTime.from_frames(96, 24)

end = start + duration
print(end.to_timecode(), "for", duration.to_seconds(), "seconds")

# Comparison rescales first, so the same instant at two rates is equal.
assert otio.opentime.RationalTime(24, 24) == otio.opentime.RationalTime(48, 48)
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/python.py</sub>

TypeScript:

```typescript
import { init, RationalTime } from "@alchemist-edit/otio";

await init();

// A time is a value and a rate, not a number of seconds. Four seconds at 24
// is 96 units; the rate travels with it so nothing has to guess later.
const start = RationalTime.fromTimecode("01:00:00:00", 24);
const duration = RationalTime.fromFrames(96, 24);

const end = start.add(duration);
console.log(`${end.toTimecode()} for ${duration.toSeconds()} seconds`);

// Comparison rescales first, so the same instant at two rates is equal.
console.assert(new RationalTime(24, 24).equal(new RationalTime(48, 48)));
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/typescript.ts</sub>

Go:

```go
package main

import (
	"fmt"
	"log"

	otio "github.com/alchemist-editor/otio-rust/sdk/go"
)

func main() {
	// A time is a value and a rate, not a number of seconds. Four seconds at
	// 24 is 96 units; the rate travels with it so nothing has to guess later.
	start, err := otio.RationalTimeFromTimecode("01:00:00:00", 24)
	if err != nil {
		log.Fatal(err)
	}
	duration := otio.RationalTimeFromFrames(96, 24)

	end := start.Add(duration)
	timecode, err := end.ToTimecode()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s for %g seconds\n", timecode, duration.ToSeconds())

	// Comparison rescales first, so the same instant at two rates is equal.
	fmt.Println(otio.RationalTime{Value: 24, Rate: 24}.Equals(otio.RationalTime{Value: 48, Rate: 48}))
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/go.go</sub>

Swift:

```swift
import OpenTimelineIO

// A time is a value and a rate, not a number of seconds. Four seconds at 24
// is 96 units; the rate travels with it so nothing has to guess later.
let start = try RationalTime.fromTimecode("01:00:00:00", rate: 24)
let duration = RationalTime.fromFrames(96, rate: 24)

let end = start.add(duration)
print(try end.toTimecode(), "for", duration.toSeconds, "seconds")

// Comparison rescales first, so the same instant at two rates is equal.
assert(RationalTime(value: 24, rate: 24).equals(RationalTime(value: 48, rate: 48)))
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/swift.swift</sub>

C++:

```cpp
#include <iostream>

#include <opentimelineio/otio.hpp>

int main() {
    // A time is a value and a rate, not a number of seconds. Four seconds at
    // 24 is 96 units; the rate travels with it so nothing has to guess later.
    const otio::RationalTime start = otio::RationalTime::from_timecode("01:00:00:00", 24);
    const otio::RationalTime duration = otio::RationalTime::from_frames(96, 24);

    const otio::RationalTime end = start.add(duration);
    std::cout << end.to_timecode() << " for " << duration.to_seconds() << " seconds\n";

    // Comparison rescales first, so the same instant at two rates is equal.
    return otio::RationalTime{24, 24}.equals(otio::RationalTime{48, 48}) ? 0 : 1;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/cpp.cpp</sub>

Zig:

```zig
const std = @import("std");
const otio = @import("otio");

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    // A time is a value and a rate, not a number of seconds. Four seconds at
    // 24 is 96 units; the rate travels with it so nothing has to guess later.
    const start = try otio.RationalTime.fromTimecode("01:00:00:00", 24);
    const duration = otio.RationalTime.fromFrames(96, 24);

    const end = start.add(duration);

    // A call that has to allocate takes the allocator first, which is the
    // only signal you need that something has to be freed.
    const timecode = try end.toTimecode(allocator);
    defer allocator.free(timecode);

    std.debug.print("{s} for {d} seconds\n", .{ timecode, duration.toSeconds() });

    // Comparison rescales first, so the same instant at two rates is equal.
    const a = otio.RationalTime{ .value = 24, .rate = 24 };
    const b = otio.RationalTime{ .value = 48, .rate = 48 };
    std.debug.assert(a.equals(b));
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/zig.zig</sub>

C:

```c
#include <stdio.h>

#include "otio.h"

int main(void) {
    /* A time is a value and a rate, not a number of seconds. Four seconds at
     * 24 is 96 units; the rate travels with it so nothing has to guess
     * later. */
    OtioRationalTime start;
    OtioBuffer error;
    if (otio_rational_time_from_timecode("01:00:00:00", 24.0, &start, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        return 1;
    }
    OtioRationalTime duration = otio_rational_time_from_frames(96.0, 24.0);

    OtioRationalTime end = otio_rational_time_add(start, duration);

    OtioBuffer timecode;
    if (otio_rational_time_to_timecode(end, &timecode, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        return 1;
    }
    printf("%s for %g seconds\n", timecode.data, otio_rational_time_to_seconds(duration));
    otio_buffer_free(timecode);

    /* Comparison rescales first, so the same instant at two rates is
     * equal. */
    OtioRationalTime a = { 24.0, 24.0 };
    OtioRationalTime b = { 48.0, 48.0 };
    return otio_rational_time_equal(a, b) ? 0 : 1;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/c.c</sub>

C#:

```csharp
using System;
using OpenTimelineIO;

// A time is a value and a rate, not a number of seconds. Four seconds at
// 24 is 96 units; the rate travels with it so nothing has to guess later.
var start = RationalTime.FromTimecode("01:00:00:00", 24);
var duration = RationalTime.FromFrames(96, 24);

var end = start.Add(duration);
Console.WriteLine($"{end.ToTimecode()} for {duration.ToSeconds()} seconds");

// Comparison rescales first, so the same instant at two rates is equal.
return new RationalTime(24, 24).Equals(new RationalTime(48, 48)) ? 0 : 1;
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/csharp.cs</sub>

Objective-C:

```objectivec
#import <Foundation/Foundation.h>

#import <OpenTimelineIO/OpenTimelineIO.h>

int main(void) {
    @autoreleasepool {
        NSError *error = nil;

        // A time is a value and a rate, not a number of seconds. Four seconds
        // at 24 is 96 units; the rate travels with it so nothing has to guess
        // later.
        //
        // A call that answers a struct has no nil to fail with, so it answers
        // through an out-parameter and says NO. That is the one place this
        // SDK reads like C rather than like Cocoa, and why.
        OTIORationalTime start;
        if (!OTIORationalTimeFromTimecode(@"01:00:00:00", 24, &start, &error)) {
            NSLog(@"%@", error);
            return 1;
        }
        OTIORationalTime duration = OTIORationalTimeFromFrames(96, 24);

        OTIORationalTime end = OTIORationalTimeAdd(start, duration);
        NSLog(@"%@ for %f seconds",
              OTIORationalTimeToTimecode(end, &error),
              OTIORationalTimeToSeconds(duration));

        // Comparison rescales first, so the same instant at two rates is equal.
        return OTIORationalTimeEquals(OTIORationalTimeMake(24, 24),
                                      OTIORationalTimeMake(48, 48))
                   ? 0
                   : 1;
    }
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/objectivec.m</sub>

An item has up to three ranges, and the difference between them is the thing
people get wrong:

- **`available_range`** — everything the media has. A two-hour rush.
- **`source_range`** — the piece of it this item uses. Four seconds of that
  rush. An item without one uses all of its media.
- **`trimmed_range`** — the source range, or the available one when there is
  no source range. What the item actually contributes.

A clip's `duration` is its trimmed range's duration; a composition's is the
sum of its children's. That is why a track knows how long it is without
anyone saying so.

There are two more — `visible_range` and `range_in_parent` — and the thing
that actually trips people up is which clock each one answers in.
[Time ranges](https://otio-rust-docs.vercel.app/docs/concepts/time-ranges/) is that, in full.

## Handles rather than pointers

Objects live in a `Document`, which owns them. You never hold an object
directly: you hold a handle, which is an index and a generation, and the
document resolves it. A handle to something that has gone away fails the
lookup rather than reading freed memory, and a handle from one document
cannot be used against another.

This is worth knowing because it shapes every binding. Every SDK but one hides
the document and lets a clip look like an object; Zig keeps it in the open,
because an arena you hand to the things that use it is how Zig already works.
Either way, what is underneath is the same arena.

Rust:

```rust
use opentime::{RationalTime, TimeRange};
use otio_core::schema::{Base, Clip, ItemData, Stack, Timeline, Track};
use otio_core::{Document, Node};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // A document is an arena. Objects go into it and are named by handles,
    // so nothing here is a pointer into anything.
    let mut document = Document::new();

    let tracks = document.insert(Node::Stack(Stack::default()));
    let timeline = document.insert(Node::Timeline(Timeline {
        base: Base {
            name: "Cut".into(),
            ..Base::default()
        },
        tracks: Some(tracks),
        global_start_time: None,
    }));
    document.set_root(Some(timeline));

    let track = document.insert(Node::Track(Track {
        kind: "Video".into(),
        ..Track::default()
    }));
    document.append_child(tracks, track)?;

    for (index, name) in ["A", "B", "C"].into_iter().enumerate() {
        let clip = document.insert(Node::Clip(Clip {
            item: ItemData {
                base: Base {
                    name: name.into(),
                    ..Base::default()
                },
                source_range: Some(TimeRange::new(
                    RationalTime::new(index as f64 * 24.0, 24.0),
                    RationalTime::new(24.0, 24.0),
                )),
                ..ItemData::new()
            },
            ..Clip::default()
        }));
        document.append_child(track, clip)?;
    }

    // Three seconds of picture, written as canonical OpenTimelineIO JSON.
    println!("{}", document.duration(track)?.to_seconds());
    std::fs::write("cut.otio", otio_core::to_string(&document)?)?;

    Ok(())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/rust.rs</sub>

Python:

```python
import opentimelineio as otio

timeline = otio.schema.Timeline(name="Cut")
track = otio.schema.Track(name="V1", kind="Video")
timeline.tracks.append(track)

for index, name in enumerate(["A", "B", "C"]):
    clip = otio.schema.Clip(name=name)
    clip.source_range = otio.opentime.TimeRange(
        start_time=otio.opentime.RationalTime(index * 24, 24),
        duration=otio.opentime.RationalTime(24, 24),
    )
    track.append(clip)

# Three seconds of picture, written as canonical OpenTimelineIO JSON.
print(track.duration().to_seconds())
otio.adapters.otio_json.write_to_file(timeline, "cut.otio")
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/python.py</sub>

TypeScript:

```typescript
import { writeFile } from "node:fs/promises";
import { init, Clip, RationalTime, Timeline, TimeRange, Track, serializeJsonToString } from "@alchemist-edit/otio";

await init();

// A fresh timeline arrives with an empty stack called `tracks`, as
// upstream's does, so there is nothing to build before appending to it.
const timeline = new Timeline({ name: "Cut" });
const track = new Track({ name: "V1", kind: "Video" });
timeline.tracks?.appendChild(track);

["A", "B", "C"].forEach((name, index) => {
  const clip = new Clip({ name });
  clip.sourceRange = new TimeRange(
    new RationalTime(index * 24, 24),
    new RationalTime(24, 24),
  );
  track.appendChild(clip);
});

// Three seconds of picture, written as canonical OpenTimelineIO JSON.
console.log(track.duration().toSeconds());
await writeFile("cut.otio", serializeJsonToString(timeline));
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/typescript.ts</sub>

Go:

```go
package main

import (
	"fmt"
	"log"

	otio "github.com/alchemist-editor/otio-rust/sdk/go"
)

func main() {
	// An object is built on its own and put together with the others
	// afterwards. Nothing has to exist before the thing it goes into.
	timeline, err := otio.NewTimeline("Cut")
	if err != nil {
		log.Fatal(err)
	}
	// A timeline is released when it is collected, so this is not required.
	// It is worth doing anyway: it frees the whole thing at once, at a
	// moment you chose.
	defer timeline.Close()

	stack, err := otio.NewStack("tracks")
	if err != nil {
		log.Fatal(err)
	}
	track, err := otio.NewTrack("V1", "Video")
	if err != nil {
		log.Fatal(err)
	}
	if err := timeline.SetTracks(&stack.Node); err != nil {
		log.Fatal(err)
	}
	if err := stack.AppendChild(track.Node); err != nil {
		log.Fatal(err)
	}

	for index, name := range []string{"A", "B", "C"} {
		clip, err := otio.NewClip(name)
		if err != nil {
			log.Fatal(err)
		}
		span := otio.TimeRange{
			StartTime: otio.RationalTime{Value: float64(index) * 24, Rate: 24},
			Duration:  otio.RationalTime{Value: 24, Rate: 24},
		}
		if err := clip.SetSourceRange(span); err != nil {
			log.Fatal(err)
		}
		// Appending moves the clip into the timeline's arena. That is
		// bookkeeping this package does for you, not something to hold.
		if err := track.AppendChild(clip.Node); err != nil {
			log.Fatal(err)
		}
	}

	// Three seconds of picture, written as canonical OpenTimelineIO JSON.
	duration, _ := track.Duration()
	fmt.Println(duration.ToSeconds())
	if err := otio.Save(timeline.Node, "cut.otio"); err != nil {
		log.Fatal(err)
	}
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/go.go</sub>

Swift:

```swift
import OpenTimelineIO

// Each object is made on its own and joins a timeline when you put it into
// one. Nothing has to exist before the thing it goes into.
let timeline = try Timeline(name: "Cut")
let stack = try Stack(name: "tracks")
let track = try Track(name: "V1", kind: "Video")

try timeline.setTracks(stack)
try stack.appendChild(track)

for (index, name) in ["A", "B", "C"].enumerated() {
    let clip = try Clip(name: name)
    try clip.setSourceRange(
        TimeRange(
            startTime: RationalTime(value: Double(index * 24), rate: 24),
            duration: RationalTime(value: 24, rate: 24)))
    try track.appendChild(clip)
}

// Three seconds of picture, written as canonical OpenTimelineIO JSON.
// Objects keep their timeline alive between them, so there is nothing to
// close.
print(try track.duration().toSeconds)
try OTIO.save(timeline, to: "cut.otio")
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/swift.swift</sub>

C++:

```cpp
#include <iostream>

#include <opentimelineio/otio.hpp>

int main() {
    // Each object is made on its own and joins a timeline when you put it
    // into one. Nothing has to exist before the thing it goes into.
    otio::Timeline timeline = otio::Timeline::create("Cut");
    otio::Stack stack = otio::Stack::create("tracks");
    otio::Track track = otio::Track::create("V1", "Video");

    timeline.set_tracks(stack);
    stack.append_child(track);

    const std::vector<std::string> names = {"A", "B", "C"};
    for (std::size_t index = 0; index < names.size(); ++index) {
        otio::Clip clip = otio::Clip::create(names[index]);
        const otio::RationalTime start(static_cast<double>(index) * 24, 24);
        clip.set_source_range(otio::TimeRange(start, otio::RationalTime(24, 24)));
        track.append_child(clip);
    }

    // Three seconds of picture, written as canonical OpenTimelineIO JSON.
    // Objects keep their timeline alive between them, so there is nothing
    // to close.
    std::cout << track.duration().to_seconds() << "\n";
    otio::save(timeline, "cut.otio");
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/cpp.cpp</sub>

Zig:

```zig
const std = @import("std");
const otio = @import("otio");

pub fn main() !void {
    // A document is an arena, and it is held in the open the way a Zig
    // programmer holds any other arena.
    const document = try otio.Document.init();
    defer document.deinit();

    const timeline = try otio.Timeline.init(document, "Cut");
    const stack = try otio.Stack.init(document, "tracks");
    const track = try otio.Track.init(document, "V1", "Video");
    try timeline.setTracks(stack.asNode());
    try stack.appendChild(track.asNode());

    for ([_][:0]const u8{ "A", "B", "C" }, 0..) |name, index| {
        const clip = try otio.Clip.init(document, name);
        try clip.setSourceRange(.{
            .start_time = .{ .value = @floatFromInt(index * 24), .rate = 24 },
            .duration = .{ .value = 24, .rate = 24 },
        });
        try track.appendChild(clip.asNode());
    }

    try document.setRoot(timeline.asNode());

    // Three seconds of picture, written as canonical OpenTimelineIO JSON.
    const duration = try track.duration();
    std.debug.print("{d}\n", .{duration.toSeconds()});
    try document.save("cut.otio");
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/zig.zig</sub>

C:

```c
#include <stdio.h>

#include "otio.h"

int main(void) {
    OtioDocument *document = otio_document_new();

    OtioNode timeline, stack, track;
    otio_timeline_new(document, "Cut", &timeline, NULL);
    otio_stack_new(document, "tracks", &stack, NULL);
    otio_track_new(document, "V1", "Video", &track, NULL);
    otio_timeline_set_tracks(document, timeline, stack, NULL);
    otio_composition_append_child(document, stack, track, NULL);

    const char *names[] = {"A", "B", "C"};
    for (size_t index = 0; index < 3; index += 1) {
        OtioNode clip;
        otio_clip_new(document, names[index], &clip, NULL);

        OtioTimeRange span;
        span.start_time.value = (double)index * 24.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);

        otio_composition_append_child(document, track, clip, NULL);
    }

    otio_document_set_root(document, timeline, NULL);

    /* Three seconds of picture, written as canonical OpenTimelineIO JSON. */
    OtioRationalTime duration;
    otio_item_duration(document, track, &duration, NULL);
    printf("%g\n", otio_rational_time_to_seconds(duration));

    otio_document_write_to_file(document, "cut.otio", otio_default_indent(), NULL);
    otio_document_free(document);
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/c.c</sub>

C#:

```csharp
using System;
using OpenTimelineIO;

// Each object is made on its own and joins a timeline when you put it
// into one. Nothing has to exist before the thing it goes into.
var timeline = new Timeline("Cut");
var stack = new Stack("tracks");
var track = new Track("V1", "Video");

timeline.SetTracks(stack);
stack.AppendChild(track);

var names = new[] { "A", "B", "C" };
for (var index = 0; index < names.Length; index++)
{
    var clip = new Clip(names[index]);
    var start = new RationalTime(index * 24, 24);
    clip.SetSourceRange(new TimeRange(start, new RationalTime(24, 24)));
    track.AppendChild(clip);
}

// Three seconds of picture, written as canonical OpenTimelineIO JSON.
// The objects keep their timeline alive between them, so there is
// nothing to dispose.
Console.WriteLine(track.Duration().ToSeconds());
Otio.Save(timeline, "cut.otio");
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/csharp.cs</sub>

Objective-C:

```objectivec
#import <Foundation/Foundation.h>

#import <OpenTimelineIO/OpenTimelineIO.h>

int main(void) {
    @autoreleasepool {
        NSError *error = nil;

        // Each object is made on its own and joins a timeline when you put
        // it into one. Nothing has to exist before the thing it goes into,
        // so these are ordinary Cocoa class factories.
        OTIOTimeline *timeline = [OTIOTimeline timelineWithName:@"Cut" error:&error];
        OTIOStack *stack = [OTIOStack stackWithName:@"tracks" error:&error];
        OTIOTrack *track = [OTIOTrack trackWithName:@"V1" kind:@"Video" error:&error];

        [timeline setTracks:stack error:&error];
        [stack appendChild:track error:&error];

        NSArray<NSString *> *names = @[ @"A", @"B", @"C" ];
        for (NSUInteger index = 0; index < names.count; index++) {
            OTIOClip *clip = [OTIOClip clipWithName:[names objectAtIndex:index] error:&error];
            OTIORationalTime start = OTIORationalTimeMake((double)index * 24, 24);
            OTIOTimeRange span = OTIOTimeRangeMake(start, OTIORationalTimeMake(24, 24));
            [clip setSourceRange:span error:&error];
            [track appendChild:clip error:&error];
        }

        // Three seconds of picture, written as canonical OpenTimelineIO JSON.
        // The objects keep their timeline alive between them, so there is
        // nothing to close.
        OTIORationalTime duration;
        if ([track getDuration:&duration error:&error]) {
            NSLog(@"%f", OTIORationalTimeToSeconds(duration));
        }
        OTIOSave(timeline, @"cut.otio", &error);
    }
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/objectivec.m</sub>

## Schemas, and what happens to ones we do not know

Every object carries a schema name and version — `Clip.2`, `Marker.3` — and
this port reads the versions upstream 0.19 writes, upgrading older ones as it
reads. An object whose schema is not recognised at all is kept verbatim, so a
third-party plugin's data survives a read and a rewrite untouched.

Writing works in the other direction too. A document can be written for an
older release by naming the schema versions it should carry, and each object
is downgraded on the way out, as upstream's serializer does. The upgrade and
downgrade steps live in one registry in the core, keyed by schema and
version, and from Python `register_upgrade_function` and
`register_downgrade_function` add to it.

A schema defined in Python with `register_type` is held in the core the way
upstream's C++ holds it: a generic object carrying its schema name, version
and fields. Python keeps the class for each schema name and hands that class
back when the object is read, so a type defined in Python round-trips through
any document without the core ever holding a Python object.

A Python subclass of a concrete schema, such as a `Clip` that also records
which take it is, stays a clip in the core: it carries the subclass's schema
name, version and extra fields beside its own, so every composition and
algorithm treats it as the clip it is, and it is written under the subclass's
name. A program that has not registered the subclass reads it as an unknown
schema, kept verbatim. Any built-in object read from a file keeps the fields
it does not know in the same place, and writes them back.

---

Page: https://otio-rust-docs.vercel.app/docs/concepts/data-model/
