# Reading and writing files

> The adapters, which formats each language can read and write, and what a round trip preserves.

An adapter turns a file of some interchange format into a document and back.
Upstream expresses this as a plugin with four loosely specified entry points
and a dictionary of keyword arguments; here it is one trait per format, with
each format's options named and typed — so a misspelled option is a compile
error rather than a silent no-op. The Python package puts upstream's own
`opentimelineio.adapters` back on top of those, with each adapter's names,
keyword arguments and exceptions, so code written against upstream runs
unchanged; there an unknown keyword is a `TypeError` rather than ignored.

| Format | Suffix | Reads | Writes | Guide |
| --- | --- | --- | --- | --- |
| OpenTimelineIO JSON | `.otio` | Yes | Yes | [The .otio file format](https://otio-rust-docs.vercel.app/docs/concepts/otio-file-format/) |
| CMX 3600 EDL | `.edl` | Yes | Yes, one video track | [EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/) |
| Avid Log Exchange | `.ale` | Yes | Yes | [ALE](https://otio-rust-docs.vercel.app/docs/formats/ale/) |
| Final Cut Pro 7 XML | `.xml` | Yes | Yes | [Final Cut Pro XML](https://otio-rust-docs.vercel.app/docs/formats/final-cut-pro/) |
| Final Cut Pro X XML | `.fcpxml` | Yes | Yes | [Final Cut Pro XML](https://otio-rust-docs.vercel.app/docs/formats/final-cut-pro/) |
| AAF | `.aaf` | Yes | Yes | [AAF](https://otio-rust-docs.vercel.app/docs/formats/aaf/) |
| OTIO zip bundle | `.otioz` | Yes, except from TypeScript | Yes, except from TypeScript | [Bundles](https://otio-rust-docs.vercel.app/docs/formats/bundles/) |
| OTIO directory bundle | `.otiod` | Yes, except from TypeScript | Yes, except from TypeScript | [Bundles](https://otio-rust-docs.vercel.app/docs/formats/bundles/) |

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 hands back whatever the file was about, usually a timeline, and the
same call shape works for every format: name the file, and pass the format's
options when it has any. Each format has its own page for what it carries and
what it cannot, linked from the table above. Two of them have a trap worth
knowing before you start: an [EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/) does not know its own
rate, and an [AAF](https://otio-rust-docs.vercel.app/docs/formats/aaf/) clip cannot be written without a MobID.

## What a round trip keeps

Reading and writing the same file back should produce the same file. Where a
format carries something the data model has no place for, the adapter keeps
it on the object's metadata under the format's own key — `metadata["cmx_3600"]`
for an EDL — so that writing it out reproduces the line it came from rather
than dropping it.

The opposite direction has limits that are the format's rather than this
library's. An EDL describes a single strand of picture, so a timeline with
two video tracks has no EDL form at all; a wipe reads as a wipe and writes
back out as a dissolve, because CMX 3600's vocabulary for wipes is not one
anybody agrees about. These are upstream's limits too, and they are
documented on each adapter rather than discovered at runtime.

## Converting between formats

Converting is a read and a write with nothing in between. There is no
conversion step, and there is no per-pair code: an EDL and an FCP X XML are
two spellings of the same object model, so once a file is read the format it
came from stops mattering.

Rust:

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

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)?;

    // Nothing happens in between. The timeline an EDL parses to is the same
    // timeline FCP X writes out, so converting is a read and a write: the
    // object model is the interchange, and the file formats are two ways of
    // spelling it.
    FcpxXml::write_to_file(&document, "cut.fcpxml", &Default::default())?;

    Ok(())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/convert-a-format/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.
timeline = otio.adapters.read_from_file("cut.edl", rate=24)

# Nothing happens in between. The timeline an EDL parses to is the same
# timeline FCP X writes out, so converting is a read and a write: the object
# model is the interchange, and the file formats are two ways of spelling it.
#
# Both calls pick their adapter from the suffix, as upstream picks it.
otio.adapters.write_to_file(timeline, "cut.fcpxml")
```

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

TypeScript:

```typescript
import { readFile, writeFile } from "node:fs/promises";
import { init, readFromString, writeToString } 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 = readFromString("cmx3600", edl, { rate: 24 });

// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the object
// model is the interchange, and the file formats are two ways of spelling it.
await writeFile("cut.fcpxml", writeToString("fcpxXml", timeline));
```

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

Go:

```go
package main

import (
	"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.
	root, err := otio.ReadFromFile(otio.FormatCMX3600, "cut.edl", &otio.ReadOptions{Rate: 24})
	if err != nil {
		log.Fatal(err)
	}
	defer root.Close()

	// Nothing happens in between. The timeline an EDL parses to is the same
	// timeline FCP X writes out, so converting is a read and a write: the
	// object model is the interchange, and the file formats are two ways of
	// spelling it.
	//
	// Writing starts at the object it is given, so handing it the root
	// writes the whole file.
	if err := otio.WriteToFile(otio.FormatFcpxXML, root, "cut.fcpxml", nil); err != nil {
		log.Fatal(err)
	}
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/convert-a-format/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)
let root = try OTIO.readFromFile(.cmx3600, path: "cut.edl", options: options)

// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the object
// model is the interchange, and the file formats are two ways of spelling it.
try OTIO.writeToFile(.fcpxXML, root: root, path: "cut.fcpxml")
```

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

C++:

```cpp
#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;

    const otio::SerializableObject root =
        otio::read_from_file(otio::Format::CMX_3600, "cut.edl", options);

    // Nothing happens in between. The timeline an EDL parses to is the same
    // timeline FCP X writes out, so converting is a read and a write: the
    // object model is the interchange, and the file formats are two ways of
    // spelling it.
    otio::write_to_file(otio::Format::FCPX_XML, root, "cut.fcpxml");

    return 0;
}
```

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

Zig:

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

pub fn 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.
    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();

    // Nothing happens in between. The timeline an EDL parses to is the same
    // timeline FCP X writes out, so converting is a read and a write: the
    // object model is the interchange, and the file formats are two ways of
    // spelling it.
    try document.writeToFile(.fcpx_xml, "cut.fcpxml", null);
}
```

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

C:

```c
#include <stdio.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;
    }

    /* Nothing happens in between. The timeline an EDL parses to is the same
     * timeline FCP X writes out, so converting is a read and a write: the
     * object model is the interchange, and the file formats are two ways of
     * spelling it. */
    if (otio_write_to_file(OTIO_FORMAT_FCPX_XML, document, "cut.fcpxml", NULL, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        otio_document_free(document);
        return 1;
    }

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

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

C#:

```csharp
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);

var timeline = Otio.ReadFromFile(Format.Cmx3600, "cut.edl", options);

// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the object
// model is the interchange, and the file formats are two ways of spelling it.
Otio.WriteToFile(Format.FcpxXml, timeline, "cut.fcpxml", null);
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/convert-a-format/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;

        OTIOSerializableObject *timeline = OTIOReadFromFile(OTIOFormatCMX3600,
                                                            @"cut.edl",
                                                            &options,
                                                            &error);
        if (timeline == nil) {
            NSLog(@"%@", error);
            return 1;
        }

        // Nothing happens in between. The timeline an EDL parses to is the
        // same timeline FCP X writes out, so converting is a read and a
        // write: the object model is the interchange, and the file formats
        // are two ways of spelling it.
        if (!OTIOWriteToFile(OTIOFormatFcpxXML, timeline, @"cut.fcpxml", NULL, &error)) {
            NSLog(@"%@", error);
            return 1;
        }
    }
    return 0;
}
```

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

What survives the trip is the intersection of the two formats, and the table
above is how to predict it. Going to an EDL narrows a cut to one video
strand whatever it came from; going the other way, an EDL carries no media
references beyond reel names, so nothing downstream can relink without being
told where the media is.

Upstream's own adapters fail outright on several of these conversions, and
here they work. An EDL whose tracks start anywhere but `00:00:00:00`, or an
audio-only EDL, could not be written as FCP X XML; neither could an FCP 7
timeline with a 15 fps clip. And FCP X XML written as FCP 7 XML produced a
file that could not be read back. Each is a documented departure on the
adapter it concerns. So is one fix to plain reading: an FCP 7 nested
sequence reads with its tracks, where upstream reads every one as an empty
stack.

## Writing what you built

`.otio` is the format with no limits — it is the data model's own
serialization, so anything you can build, it can hold.

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>

---

Page: https://otio-rust-docs.vercel.app/docs/guides/reading-and-writing/
