# Overview

> A pure-Rust OpenTimelineIO, and a binding for every language it is generated into.

[OpenTimelineIO](https://opentimeline.io) is how editorial decisions move
between programs. A cut assembled in one tool, a conform in another, a review
in a third: each holds the same idea — these shots, in this order, trimmed
here, at this rate — and OTIO is the written form of that idea, so the three
can agree about it without agreeing about anything else.

This library is a port of it to Rust. Not a reimplementation with its own
ideas about how timelines should work: the arithmetic, the rounding and the
timecode behaviour follow upstream exactly, measured against upstream's own
test suites, because a file written here has to open unchanged in every tool
that already reads OTIO.

## What you get

The whole data model, the time math beneath it, and the interchange formats
post actually uses:

| Piece | What it does |
| --- | --- |
| `opentime` | Rational time, time ranges, SMPTE timecode, drop-frame and all |
| `otio-core` | The schemas, the compositions, the algorithms, and `.otio` JSON |
| `otio-ale` | Avid Log Exchange |
| `otio-cmx3600` | CMX 3600 Edit Decision Lists |
| `otio-fcp7` | Final Cut Pro 7 interchange XML |
| `otio-fcpx` | Final Cut Pro X XML |
| `otio-aaf` | AAF, read and written as upstream's adapter writes it |

Everything above sits on Rust and nothing else. There is no C++ library
underneath and no Python interpreter: a binding links one static library.

## Why the bindings all agree

Each language SDK is generated from the C interface rather than written by
hand, and that interface describes itself — the generator reads the Rust
source of the ABI, not a header and not a schema somebody maintains alongside
it. So a call added to the core is a call every SDK has, and a call whose
meaning changed cannot keep an old signature in one language and a new one in
another. Drift fails the build.

That is also why this site can promise a switcher rather than a paragraph
apologising for the languages it did not get to.

Rust:

```rust
use otio_adapter::Adapter;
use otio_cmx3600::{Cmx3600, ReadOptions};

fn main() -> Result<(), otio_adapter::Error> {
    // An EDL never says what rate its timecode is at, so this has to be
    // right: a file read at the wrong rate puts every event in the wrong
    // place rather than failing.
    let options = ReadOptions {
        rate: 24.0,
        ..Default::default()
    };
    let document = Cmx3600::read_from_file("cut.edl", &options)?;

    let root = document.root().expect("a parsed document has a root");
    for id in document.find_clips(root)? {
        let clip = document.try_get(id)?;
        println!("{}", clip.name());
    }

    Ok(())
}
```

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

Python:

```python
import opentimelineio as otio

# An EDL never says what rate its timecode is at, so this has to be right: a
# file read at the wrong rate puts every event in the wrong place rather than
# failing. The adapter is picked from the suffix, as upstream picks it.
timeline = otio.adapters.read_from_file("cut.edl", rate=24)

for clip in timeline.find_clips():
    print(clip.name)
```

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

TypeScript:

```typescript
import { readFile } from "node:fs/promises";
import { init, readTimelineFromString } from "@alchemist-edit/otio";

await init();

// There is no filesystem behind a WebAssembly module, so getting hold of the
// bytes is the host's line of code: `fs` in Node, `fetch` in a browser.
const edl = await readFile("cut.edl", "utf8");

// An EDL never says what rate its timecode is at, so this has to be right: a
// file read at the wrong rate puts every event in the wrong place rather
// than failing.
const timeline = readTimelineFromString("cmx3600", edl, { rate: 24 });

for (const clip of timeline.findClips()) {
  console.log(clip.name);
}
```

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

Go:

```go
package main

import (
	"fmt"
	"log"

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

func main() {
	// An EDL never says what rate its timecode is at, so this has to be
	// right: a file read at the wrong rate puts every event in the wrong
	// place rather than failing.
	//
	// Reading answers with what the file was about — the root object — and
	// there is no container to hold on to besides it.
	root, err := otio.ReadFromFile(otio.FormatCMX3600, "cut.edl", &otio.ReadOptions{Rate: 24})
	if err != nil {
		log.Fatal(err)
	}
	defer root.Close()

	clips, err := root.FindClips()
	if err != nil {
		log.Fatal(err)
	}
	for _, clip := range otio.Filter(clips, otio.Node.AsClip) {
		name, _ := clip.Name()
		fmt.Println(name)
	}
}
```

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

Swift:

```swift
import OpenTimelineIO

// An EDL never says what rate its timecode is at, so this has to be right: a
// file read at the wrong rate puts every event in the wrong place rather
// than failing.
let options = ReadOptions(rate: 24, nameColumn: "", ignoreTimecodeMismatch: false)

// Reading hands back what the file is about, not a container to look inside.
let root = try OTIO.readFromFile(.cmx3600, path: "cut.edl", options: options)

for case let clip as Clip in try root.findClips() {
    print(try clip.name())
}
```

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

C++:

```cpp
#include <iostream>

#include <opentimelineio/otio.hpp>

int main() {
    // An EDL never says what rate its timecode is at, so this has to be
    // right: a file read at the wrong rate puts every event in the wrong
    // place rather than failing.
    otio::ReadOptions options = otio::read_options_default();
    options.rate = 24;

    // Reading hands back the file's root object; there is no container
    // around it to hold.
    const otio::SerializableObject root =
        otio::read_from_file(otio::Format::CMX_3600, "cut.edl", options);

    for (const otio::SerializableObject &node : root.find_clips()) {
        if (std::optional<otio::Clip> clip = node.as<otio::Clip>()) {
            std::cout << clip->name() << "\n";
        }
    }

    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/read-an-edl/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();

    // An EDL never says what rate its timecode is at, so this has to be
    // right: a file read at the wrong rate puts every event in the wrong
    // place rather than failing.
    const options = otio.ReadOptions{
        .rate = 24,
        .name_column = null,
        .ignore_timecode_mismatch = false,
    };

    const document = try otio.Document.readFromFile(.cmx3600, "cut.edl", options);
    defer document.deinit();

    const root = (try document.root()) orelse return error.Empty;

    // Anything the library hands back that has to be freed is copied into an
    // allocator you pass, and freed with `allocator.free`.
    const clips = try root.findClips(allocator);
    defer allocator.free(clips);

    for (clips) |node| {
        if (node.asClip()) |clip| {
            const name = try clip.name(allocator);
            defer allocator.free(name);
            std.debug.print("{s}\n", .{name});
        }
    }
}
```

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

C:

```c
#include <stdio.h>
#include <stdlib.h>

#include "otio.h"

int main(void) {
    /* An EDL never says what rate its timecode is at, so this has to be
     * right: a file read at the wrong rate puts every event in the wrong
     * place rather than failing. */
    OtioReadOptions options;
    options.struct_size = sizeof options;
    otio_read_options_init(&options);
    options.rate = 24;

    /* A call that can fail says why in the buffer passed last, which is
     * yours to free. Pass NULL where the status is all you want. */
    OtioDocument *document = NULL;
    OtioBuffer error;
    if (otio_read_from_file(OTIO_FORMAT_CMX_3600, "cut.edl", &options, &document, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        return 1;
    }

    OtioNode root;
    otio_document_root(document, &root, NULL);

    /* A list call is asked twice: once with no buffer, to learn the count,
     * and once to fill one. */
    size_t count = 0;
    otio_node_find_clips(document, root, NULL, 0, &count, NULL);

    OtioNode *clips = malloc(count * sizeof *clips);
    otio_node_find_clips(document, root, clips, count, &count, NULL);

    for (size_t index = 0; index < count; index += 1) {
        OtioBuffer name;
        if (otio_node_name(document, clips[index], &name, NULL) == OTIO_STATUS_OK) {
            printf("%s\n", name.data);
            otio_buffer_free(name);
        }
    }

    free(clips);
    otio_document_free(document);
    return 0;
}
```

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

C#:

```csharp
using System;
using OpenTimelineIO;

// An EDL never says what rate its timecode is at, so this has to be
// right: a file read at the wrong rate puts every event in the wrong
// place rather than failing.
// A value here is a `readonly struct`, as one in .NET should be, so the
// rate is chosen when the options are made rather than set afterwards.
var defaults = Otio.ReadOptionsDefault();
var options = new ReadOptions(24, defaults.NameColumn, defaults.IgnoreTimecodeMismatch);

// Reading hands back the object the file is about, which for an EDL is
// the timeline it describes.
var timeline = Otio.ReadFromFile(Format.Cmx3600, "cut.edl", options);

foreach (var node in timeline.FindClips())
{
    if (node is Clip clip)
    {
        Console.WriteLine(clip.Name());
    }
}
```

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

Objective-C:

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

#import <OpenTimelineIO/OpenTimelineIO.h>

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

        // An EDL never says what rate its timecode is at, so this has to be
        // right: a file read at the wrong rate puts every event in the wrong
        // place rather than failing.
        OTIOReadOptions options = OTIOReadOptionsDefault();
        options.rate = 24;

        // Reading hands back the object the file is about, which for an EDL
        // is the timeline it describes.
        OTIOSerializableObject *timeline = OTIOReadFromFile(OTIOFormatCMX3600,
                                                            @"cut.edl",
                                                            &options,
                                                            &error);
        if (timeline == nil) {
            NSLog(@"%@", error);
            return 1;
        }

        for (OTIOSerializableObject *node in [timeline findClips:&error]) {
            // Every object arrives as the class its schema names, so this is
            // an ordinary Cocoa class test rather than a cast.
            if ([node isKindOfClass:[OTIOClip class]]) {
                NSLog(@"%@", [node name:&error]);
            }
        }
    }
    return 0;
}
```

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

Reading is the same shape everywhere: reading a file hands back the object it
is about, and you ask that object what is under it. Only Rust, C and Zig show
you the document that owns the objects on the way; every other SDK keeps it
out of sight, as upstream's own bindings do. What differs is what each
language calls a failure and what it calls an absence — an `error` in Go, a
`throws` in Swift, an `NSError` in Objective-C, an optional in Zig — and the
SDK for each follows that language rather than the C interface it came from.

## Where to go next

- [Getting started](https://otio-rust-docs.vercel.app/docs/getting-started/) builds the core and reaches it from
  each language, and each language has [its own page](https://otio-rust-docs.vercel.app/languages/).
- [Reading and writing files](https://otio-rust-docs.vercel.app/docs/guides/reading-and-writing/) is the guide to
  the adapters, with a page per format: [EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/),
  [ALE](https://otio-rust-docs.vercel.app/docs/formats/ale/), [Final Cut Pro XML](https://otio-rust-docs.vercel.app/docs/formats/final-cut-pro/),
  [AAF](https://otio-rust-docs.vercel.app/docs/formats/aaf/) and [bundles](https://otio-rust-docs.vercel.app/docs/formats/bundles/).
- [Editing a timeline](https://otio-rust-docs.vercel.app/docs/guides/editing/) covers the ten edit operations.
- [The data model](https://otio-rust-docs.vercel.app/docs/concepts/data-model/) and
  [Time ranges](https://otio-rust-docs.vercel.app/docs/concepts/time-ranges/) are the concepts every other page
  assumes.
- [The C ABI reference](https://otio-rust-docs.vercel.app/reference/) lists every call the SDKs are generated
  from.

For a language model, [`/llms.txt`](https://otio-rust-docs.vercel.app/llms.txt) indexes every page as Markdown,
and `/llms/<language>.txt` is the whole of the docs with every sample in one
language.

---

Page: https://otio-rust-docs.vercel.app/docs/
