Editorial timelines, in every language you ship in.
A pure-Rust OpenTimelineIO — the data model, the time math, and the adapters for EDL, ALE, Final Cut and AAF — with 10 bindings generated from one C interface of 338 calls.
Reading a cut and asking what is in it — pick your language, and the whole site follows.
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(())
}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)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);
}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)
}
}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())
} <iostream>
<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;
}const std = ("std");
const otio = ("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});
}
}
}
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;
}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());
}
}
int main(void) {
{
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;
}Start here
Get started
Build the core and reach it from your language, or install the npm package.
Read and write files
EDL, ALE, Final Cut Pro XML, AAF and bundles: what each format carries and loses.
Understand the model
Timelines, tracks, clips and the five ranges every item has.
Look up a call
Every call in the C ABI the SDKs are generated from, with its declaration.
Pick your language
A port, not a rewrite
The arithmetic, the rounding and the timecode behaviour follow upstream exactly, measured against upstream’s own test suites.
No C++, no Python underneath
The core is Rust the whole way down. A binding loads one Rust library and nothing else.
Every SDK is generated
One description of the C ABI, read out of its own source, becomes the SDK for each language. Drift fails the build.