O
All pages

The .otio file format

What is in an OTIO JSON file, what this implementation guarantees about it, and where it departs from strict JSON.

.otio is the format the rest of OpenTimelineIO exists to read and write. It is JSON, it is meant to be read by a person as well as a program, and it is the contract between this implementation and every other one.

That contract matters more here than it does upstream. Upstream is the reference implementation, so whatever it writes is by definition right; this is a second implementation, and a file it writes has to open unchanged in tools built on the first.

The shape

An OTIO file is a tree of objects. Each object is a JSON dictionary whose fields are either plain JSON values or more OTIO objects.

Every object carries an OTIO_SCHEMA key naming its type and version:

{
  "OTIO_SCHEMA": "Timeline.1",
  "name": "my timeline",
  "metadata": {},
  "tracks": {
    "OTIO_SCHEMA": "Stack.1",
    "name": "tracks",
    "children": [
      {
        "OTIO_SCHEMA": "Track.1",
        "name": "video track",
        "kind": "Video",
        "children": []
      }
    ]
  }
}

There is no file-level version. Each type is versioned on its own, so a new Clip.3 can land without renumbering everything else. That is also why reading starts by looking at whatever the top-level object says it is rather than at a header.

Children live under children, everywhere except a timeline, whose one child is tracks.

The top level can be anything

Most files hold a timeline, and most code assumes one. The format does not require it: a bare Clip, a Track, even a RationalTime is a valid .otio file, and this library writes one as readily.

otio_core::to_string starts at whatever it is pointed at.
Hand it a clip and you get a clip.

Code that reads a file should say so when the top-level object is not what it wanted, rather than assuming. A SerializableCollection is the right container for several objects, because a bare JSON array cannot carry metadata of its own.

No instancing

There are no references to an object from two places. If the same clip or the same media appears twice in a timeline, it appears as two identical copies.

This is worth knowing before you go looking for a way to share one: the format does not have it, and neither do the objects in memory — see the data model for how handles work here.

The reader does understand the OTIO_REF_ID tags and SerializableObjectRef pointers that some files carry in metadata, and holds them to upstream's rule: two objects declaring the same OTIO_REF_ID make the file unreadable, with Duplicated object reference while reading: near line N, because a reference could not say which of the two it meant.

Metadata

Nearly every object has a metadata dictionary, holding anything JSON can hold, nested as deeply as you like, including other OTIO objects.

The core does nothing with it at all. It carries it, unchanged, so adapters and tools can keep what the schema has no field for — which several of the adapters here do.

Namespace it. Two workflows writing to the same file will collide unless each keeps to its own key:

"metadata": {
  "my_playback_tool": { "loop": false },
  "my_production_tracking_system": { "status": "IP", "owner": "rose" }
}

It is also where a new schema usually starts life: put the field in metadata, see whether it earns its place, then move it.

What this implementation guarantees

  • Field order is upstream's, not alphabetical and not this library's convenience. Fields come out in the order upstream's own write_to methods emit them, so a diff between a file written here and one written there is empty rather than a reordering.
  • Indentation is four spaces by default, as upstream's is. The format permits minifying and the project recommends against it: gzip the file if size matters, and keep it readable.
  • Round trips are byte-for-byte on upstream's own sample documents, which is how that first point is measured rather than asserted.

Where it is not quite JSON

Upstream configures RapidJSON with kWriteNanAndInfFlag, so a non-finite number is written as a bare NaN, Infinity or -Infinity literal. None of those is valid JSON, and a strict parser will refuse the file — including upstream's own, unless it is the one doing the reading.

This library reproduces it exactly, in both directions: it writes those literals for non-finite values and accepts them when reading, in the spellings RapidJSON accepts (Inf as well as Infinity).

That is deliberate, and it is the project's rule in miniature. Writing a tidier null instead would quietly change a document that somebody else's tool produced, and agreeing with upstream is the only way to agree with every other reader. The place to be surprised by this is here, in the documentation, rather than in a file that will not load.

Naming

Files should end in .otio. Not .json — the suffix is how every adapter in the ecosystem, this one included, decides what it is holding.


This page is adapted from the File Format Specification in upstream OpenTimelineIO's documentation, which is Apache-2.0 like this project. The guarantees section describes this implementation and has no counterpart upstream.