# OpenTimelineIO for Rust: the C documentation > A pure-Rust OpenTimelineIO: the data model, the time math, the file-format adapters, and an SDK for every language they are generated into. The documentation with every code sample in C. The ABI every other SDK is generated from. Out-parameters and status codes. The binding lives in `crates/otio-capi` (https://github.com/alchemist-editor/otio-rust/blob/main/crates/otio-capi). Index of everything: https://otio-rust-docs.vercel.app/llms.txt ## C > libotio and otio.h, the C ABI every generated SDK is built on, with handles, out-parameters and status codes. The C ABI is `libotio`, plus the header in `crates/otio-capi/include/otio.h`. It is the layer every language binding other than Python sits on: Go, Swift, Zig, C++, C# and Objective-C link it, and TypeScript is the same ABI compiled to WebAssembly. From C you use it directly, which means the document is explicit, values are copied out into buffers you free, and every call that can fail returns a status. ### Install ```sh cargo build -p otio-capi --release ``` That writes `libotio.so`, `libotio.dylib` or `otio.dll` alongside the static `libotio.a`, under `target/release/`. Compile against `include/otio.h` and link `otio`. Each [GitHub release](https://github.com/alchemist-editor/otio-rust/releases) carries a prebuilt `libotio--.tar.gz` per target, holding `include/otio.h`, `lib/` (static, and shared where built) and the licence. The targets are `x86_64-linux-gnu`, `aarch64-linux-gnu`, `aarch64-macos`, `x86_64-macos`, `x86_64-windows-msvc`, `aarch64-windows-msvc` and `x86_64-windows-gnu`, the last static only. A `SHA256SUMS` file covers every asset. ### Your first program ```c #include #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/read-an-edl/c.c Most of what the C ABI asks of a caller is in that program. The format is an `OtioFormat`, and the options are sized and then filled with their defaults by `otio_read_options_init()`; the rate is the one an EDL cannot do without (see [CMX 3600 EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/)). A list is asked for twice, once with a capacity of zero to learn the count and once to fill a buffer of that size. A string comes back as an `OtioBuffer` the caller frees with `otio_buffer_free`. ### Building a timeline ```c #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/c.c `otio_document_new` makes a document, and every constructor, such as `otio_clip_new`, takes the document to build the object in. The root is set explicitly with `otio_document_set_root`, and `otio_document_free` releases the document and everything in it. ### How objects and documents work here **An object is a handle, not a pointer.** `OtioNode` is the index of a slot in the document's arena and the generation of that slot. Nothing hands out a pointer into a document, so an edit that moves objects around cannot leave a caller holding a dangling one, and a handle to something that has been removed fails a lookup instead of reaching whatever took the slot. Within one document, two `OtioNode` values compare equal when they name the same object, and that is the whole of object identity here. A handle is scoped to the document that issued it: in a copy made by `otio_document_clone` the same handle names the copy of the same object, and a handle passed to some other document is a programming error the library cannot always catch. **A document is explicit.** There is one document, created and freed when the caller says. Each document owns its objects. `otio_document_absorb` moves every object of one document into another and hands back each object's old and new handle; otherwise, build in one document, or write and read. **A value is copied out, never borrowed.** Times are plain structs passed by value; strings and whole written files come back as owned `OtioBuffer`s. Returning a pointer into the document would dangle the moment anything was edited. **Threads.** A document is not internally synchronized. Several threads may read one at the same time; a thread that edits one must be the only thread touching it. Two threads working on two documents never interfere. ### Errors and missing values Every call that can fail returns an `OtioStatus`, delivers its result through out-parameters, and takes one more out-parameter last: `OtioBuffer *out_error`, where it writes the sentence saying what went wrong. `OTIO_STATUS_OK` is zero, so `if (otio_...(...))` reads as "if it failed". Pass `NULL` to get only the status. When it is not `NULL` it is written on every return, empty after success, so a caller can free it unconditionally. The message comes back from the call that failed rather than from a second call asking about "the last failure", so it does not matter which thread asks. `OTIO_STATUS_NO_VALUE` means the question was answered and the answer is "nothing", which is what an item with no source range reports. It is not an error, but it still carries a message saying what had no value. A panic in the Rust core is caught at the boundary and reported as `OTIO_STATUS_PANIC` rather than unwinding into C. `OTIO_STATUS_WRONG_KIND` means the call was asked of the wrong kind of thing: a clip asked for its children or a track's kind, a metadata list stepped into by key, or a metadata string read as a number. That is almost always a mistake in the calling code, so it is kept apart from `OTIO_STATUS_CORE_ERROR`, which is the document itself refusing, such as a clip with no media asked for its available range. ### Editing and algorithms ```c #include #include #include "otio.h" /* One second of picture, named. */ static OtioNode second(OtioDocument *document, const char *name) { OtioNode clip; otio_clip_new(document, name, &clip, NULL); OtioTimeRange span; span.start_time.value = 0.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); return clip; } static void show(OtioDocument *document, OtioNode track) { size_t count = 0; otio_node_children(document, track, NULL, 0, &count, NULL); OtioNode *children = malloc(count * sizeof *children); otio_node_children(document, track, children, count, &count, NULL); for (size_t index = 0; index < count; index += 1) { OtioBuffer name; if (otio_node_name(document, children[index], &name, NULL) == OTIO_STATUS_OK) { printf("%s%s", index == 0 ? "" : " ", name.data); otio_buffer_free(name); } } free(children); OtioRationalTime duration; otio_item_duration(document, track, &duration, NULL); printf(" - %g frames\n", duration.value); } int main(void) { OtioDocument *document = otio_document_new(); OtioNode track; otio_track_new(document, "V1", "Video", &track, NULL); const char *names[] = {"A", "B", "C"}; for (size_t index = 0; index < 3; index += 1) { otio_composition_append_child(document, track, second(document, names[index]), NULL); } show(document, track); /* Insert makes room: everything from the insertion point onwards moves * later, and the track gets longer. */ OtioRationalTime at = {24.0, 24.0}; otio_edit_insert(document, second(document, "D"), track, at, false, otio_node_none(), NULL); show(document, track); /* Overwrite does not: it lays an item over a span and whatever was in * that span gives way. The track is the same length afterwards. */ OtioTimeRange over; over.start_time.value = 48.0; over.start_time.rate = 24.0; over.duration.value = 24.0; over.duration.rate = 24.0; otio_edit_overwrite(document, second(document, "E"), track, over, false, otio_node_none(), NULL); show(document, track); otio_document_free(document); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/edit-operations/c.c The ten edit operations are `otio_edit_*` calls, such as `otio_edit_insert` and `otio_edit_overwrite`, and the algorithms are `otio_algorithm_*`, such as `otio_algorithm_flatten_stack`. [Editing a timeline](https://otio-rust-docs.vercel.app/docs/guides/editing/) says what each edit does. ### Formats Every format goes through the same four calls, `otio_read_from_bytes`, `otio_read_from_file`, `otio_write_to_bytes` and `otio_write_to_file`, picked by an `OtioFormat`: OTIO JSON, ALE, CMX 3600 EDL, both FCP XML flavours, AAF, and the two bundle formats, `.otioz` and `.otiod`. A bundle is read and written through a path only; the calls that take bytes refuse one with `OTIO_STATUS_UNSUPPORTED`. What is written is the document's root, which for a bundle has to be a timeline. ```c #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/convert-a-format/c.c A few adapter options are not exposed yet: ALE's explicit column order, AAF's `transcribe_log`, and upstream's bundle `dry_run`. See [Reading and writing files](https://otio-rust-docs.vercel.app/docs/guides/reading-and-writing/), [AAF](https://otio-rust-docs.vercel.app/docs/formats/aaf/) and [Bundles](https://otio-rust-docs.vercel.app/docs/formats/bundles/). Metadata is named by a path rather than a key, such as `"cmx_3600.reel"` or `"comments[0]"`, read with calls such as `otio_metadata_get_string`. A key that holds a `.`, a `[` or a `\` writes it after a `\`: `"com\.avid\.reel"` is the single key `com.avid.reel`. The header states the full grammar under METADATA PATHS. ### Options structs `OtioReadOptions` and `OtioWriteOptions` gain fields as formats gain options, so each opens with `size_t struct_size`. Set it to the struct's size, call `otio_read_options_init()` or `otio_write_options_init()` to fill in every field's default, and change only the fields you need. The library reads only the fields that size covers and takes its defaults for the rest, so a program built against an older header keeps working with a newer library. A struct zeroed by hand has no size and is refused with `OTIO_STATUS_INVALID_ARGUMENT`. The defaults are filled into the caller's struct rather than returned, because a struct returned by value has the size the library was built with. A newer library returning its larger struct would write past the end of an older program's. ### Media linkers and hooks `otio_register_media_linker` and `otio_register_hook_script` take an `OtioPluginFn`, a `void *context` handed to every call, and an `OtioPluginReleaseFn` the library calls with that context once nothing will call the plugin again. The function is handed the document, the clip or the hook's target, an object whose metadata is the arguments, a place for its answer, and room for a message. Return a status other than `OTIO_STATUS_OK`, with the reason written into `message`, to fail the read; the caller sees `OTIO_STATUS_PLUGIN_ERROR`. A linker that leaves `out_result` alone leaves the clip as it was. A read names its linker in `OtioReadOptions.media_linker` and carries the argument maps as JSON in `media_linker_arguments` and `hook_arguments`. The document is the library's for the length of the call: answer with an object in it, and keep no handle past the call. See [Media linkers and hooks](https://otio-rust-docs.vercel.app/docs/guides/media-linkers-and-hooks/). ### Platforms CI compiles and runs the C ABI's own C test program on Linux and macOS. The library builds on Windows and its struct layouts are asserted from the Rust side, but no C program is linked there in CI. A WebAssembly build for the browser has no file system, so every call that takes a path answers `OTIO_STATUS_UNSUPPORTED` there; the calls that take bytes work everywhere. A WASI build has a file system and keeps the path calls. ### Versions `otio_version()` is the release. `OTIO_ABI_VERSION`, from the header, and `otio_abi_version()`, from the library, are the version of the binary interface; check at startup that they agree: ```c if (otio_abi_version() != OTIO_ABI_VERSION) { fprintf(stderr, "libotio %s is not the one this program was built for\n", otio_version()); return 1; } ``` The ABI version is 0 until the ABI is declared stable, so for now any release may change it; insist on the exact `otio_version()` you built against too. From version 1, a release names the shared library for its ABI version (`libotio.so.1`, `libotio.1.dylib`, `otio1.dll`) so that two can be installed side by side. Every enum is open: a later release may add values, such as a new `OtioStatus` or `OtioNodeKind`, without changing the ABI version. Handle a value you do not know with a `default:` rather than assuming the set is closed. `OTIO_NODE_KIND_OTHER` and `OTIO_VALUE_OTHER` exist so that a newer core reports something honest to a caller built against an older header. That promise is to C callers: the generated SDKs are made from one release's header and are meant to run with that release's library, which they ship with or pin. `otio_node_kind` reports the most specific kind it knows: a clip is `OTIO_NODE_KIND_CLIP`, never `OTIO_NODE_KIND_ITEM`. ### Reference - [`crates/otio-capi/README.md`](https://github.com/alchemist-editor/otio-rust/blob/main/crates/otio-capi/README.md): the four decisions the ABI makes, and how the header is kept honest. - [The reference](https://otio-rust-docs.vercel.app/reference/), generated from the same description as every SDK, with each declaration lifted verbatim from `otio.h`. - [How the SDKs are made](https://otio-rust-docs.vercel.app/docs/internals/how-the-sdks-are-made/). --- Page: https://otio-rust-docs.vercel.app/docs/languages/c/ ## Start here ### 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. ```c #include #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/read-an-edl/c.c 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/.txt` is the whole of the docs with every sample in one language. --- Page: https://otio-rust-docs.vercel.app/docs/ ### Getting started > Building the core, and reaching it from your language. Everything starts with the Rust core. The bindings other than Python link a static library built from it, so that is the first build in every case. The TypeScript package is the exception: it is [on npm](#typescript), and needs none of this. This page is the short version for every language at once. Each language has a page of its own with its install steps, a first program, and how it spells failure and absence: [Rust](https://otio-rust-docs.vercel.app/docs/languages/rust/), [Python](https://otio-rust-docs.vercel.app/docs/languages/python/), [TypeScript](https://otio-rust-docs.vercel.app/docs/languages/typescript/), [Go](https://otio-rust-docs.vercel.app/docs/languages/go/), [Swift](https://otio-rust-docs.vercel.app/docs/languages/swift/), [C++](https://otio-rust-docs.vercel.app/docs/languages/cpp/), [Zig](https://otio-rust-docs.vercel.app/docs/languages/zig/), [C](https://otio-rust-docs.vercel.app/docs/languages/c/), [C#](https://otio-rust-docs.vercel.app/docs/languages/csharp/) and [Objective-C](https://otio-rust-docs.vercel.app/docs/languages/objectivec/). ```sh git clone https://github.com/alchemist-editor/otio-rust cd otio-rust cargo build --workspace cargo test --workspace ``` > [!NOTE] > Build the workspace before running the tests that include the C ABI's own C > program. It links against `libotio`, so a test run that is also the first > build can reach for a library that is not there yet. #### Rust Add the crates you need. `otio-core` is the data model; each adapter is its own crate so that a program reading EDLs does not compile an AAF parser. ```toml [dependencies] otio-core = { git = "https://github.com/alchemist-editor/otio-rust" } otio-cmx3600 = { git = "https://github.com/alchemist-editor/otio-rust" } ``` #### Python The package is called `opentimelineio`, and the name is the point: it aims to be a drop-in replacement for upstream's own package, measured by running upstream's test files unmodified. ```sh cd crates/otio-python pip install . ``` Files are read and written the way upstream reads and writes them, through `opentimelineio.adapters`: `read_from_file("cut.edl", rate=24)` picks the EDL adapter from the suffix, and each adapter takes upstream's keyword arguments. Every format on the [reading and writing](https://otio-rust-docs.vercel.app/docs/guides/reading-and-writing/) page is there, AAF included. The rest of upstream's package is there as well. Its plugin system loads adapters, media linkers, hooks, schemadefs and version manifests from `OTIO_PLUGIN_MANIFEST_PATH` and from installed packages' entry points, the same way upstream's does, and the built-in formats are declared as plugins themselves. Installing the package also installs upstream's console tools: `otiocat`, `otioconvert`, `otiostat`, `otiotool` and `otiopluginfo`. #### The SDKs Go, Swift, Zig, C++, C# and Objective-C each link `libotio`, which is not checked in. Build it and put it where the package expects it: ```sh cargo build -p otio-capi --release cp target/release/libotio.a sdk/go/lib/ # or sdk/swift, sdk/zig, sdk/cpp, sdk/objc ``` C# is the one that loads the library at run time rather than linking it, so it wants the shared one instead — `libotio.so`, or `libotio.dylib` on macOS — copied into `sdk/csharp/lib/`. Then each is ordinary for its language — `go test ./...`, `swift test`, `zig build test`, `cmake --build`, `dotnet run --project tests`, `make check`. The README beside each SDK has the exact invocation, including the linker flag Swift needs and the GNUstep packages Objective-C needs on Linux. #### TypeScript The TypeScript package is different: it has no static library at all. The C ABI is compiled to WebAssembly and the package ships the module, so it runs in a browser and in Node with nothing native to install. It is published to npm as `@alchemist-edit/otio`, so none of the build above is needed to use it: ```sh npm install @alchemist-edit/otio ``` Call `init()` once before anything else. In Node it reads the module off disk; in a browser it fetches it from beside the JavaScript, in the form every bundler understands, so there is nothing to configure. To build it from this repository instead, which needs Rust and the `wasm32-unknown-unknown` target: ```sh cd crates/otio-wasm/ts npm install npm run build ``` There is no filesystem behind a WebAssembly module, so the adapters there take and return bytes and leave getting hold of them to the host — a `fetch` in a browser, `fs` in Node. That is a line of code, and it is the host's line rather than the library's. --- Page: https://otio-rust-docs.vercel.app/docs/getting-started/ ## Guides ### 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/) | ```c #include #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/read-an-edl/c.c 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. ```c #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/convert-a-format/c.c 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. ```c #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/c.c --- Page: https://otio-rust-docs.vercel.app/docs/guides/reading-and-writing/ ### Editing a timeline > The ten edit operations, what each one does to its neighbours, and which of them change a track's length. Appending clips to a track builds a cut. Changing one is a different problem, because every change has a question attached: what happens to everything after it? There are ten operations, and they are the ones a non-linear editor has. They are functions rather than methods, because each is about two objects and belongs to neither — which is how upstream arranges them too. ```c #include #include #include "otio.h" /* One second of picture, named. */ static OtioNode second(OtioDocument *document, const char *name) { OtioNode clip; otio_clip_new(document, name, &clip, NULL); OtioTimeRange span; span.start_time.value = 0.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); return clip; } static void show(OtioDocument *document, OtioNode track) { size_t count = 0; otio_node_children(document, track, NULL, 0, &count, NULL); OtioNode *children = malloc(count * sizeof *children); otio_node_children(document, track, children, count, &count, NULL); for (size_t index = 0; index < count; index += 1) { OtioBuffer name; if (otio_node_name(document, children[index], &name, NULL) == OTIO_STATUS_OK) { printf("%s%s", index == 0 ? "" : " ", name.data); otio_buffer_free(name); } } free(children); OtioRationalTime duration; otio_item_duration(document, track, &duration, NULL); printf(" - %g frames\n", duration.value); } int main(void) { OtioDocument *document = otio_document_new(); OtioNode track; otio_track_new(document, "V1", "Video", &track, NULL); const char *names[] = {"A", "B", "C"}; for (size_t index = 0; index < 3; index += 1) { otio_composition_append_child(document, track, second(document, names[index]), NULL); } show(document, track); /* Insert makes room: everything from the insertion point onwards moves * later, and the track gets longer. */ OtioRationalTime at = {24.0, 24.0}; otio_edit_insert(document, second(document, "D"), track, at, false, otio_node_none(), NULL); show(document, track); /* Overwrite does not: it lays an item over a span and whatever was in * that span gives way. The track is the same length afterwards. */ OtioTimeRange over; over.start_time.value = 48.0; over.start_time.rate = 24.0; over.duration.value = 24.0; over.duration.rate = 24.0; otio_edit_overwrite(document, second(document, "E"), track, over, false, otio_node_none(), NULL); show(document, track); otio_document_free(document); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/edit-operations/c.c #### The ten | Operation | What it does | | --- | --- | | `insert` | Puts an item at an instant, pushing what follows later. | | `overwrite` | Lays an item over a span, replacing what was there. | | `trim` | Moves an item's in and out points without moving its neighbours. | | `ripple` | Moves an item's in and out points, sliding everything after it. | | `roll` | Moves the cut between an item and its neighbour. | | `slip` | Moves the media inside an item without moving the item. | | `slide` | Moves an item along its track, taking the time from its neighbours. | | `slice` | Cuts whatever sits at an instant into two. | | `fill` | Drops an item into a gap, fitted as the reference point says. | | `remove` | Takes whatever sits at an instant out. | #### The question each one answers The useful way to hold these is by what they do to the track's length and to the neighbours. **Length changes.** `insert` makes the track longer by the length of what went in. `remove` without a fill makes it shorter, because what follows moves up; `remove` with a fill leaves a gap and the length alone. `ripple` changes it by however much the in or out point moved. **Length does not change.** `overwrite` replaces rather than displaces. `trim`, `roll`, `slip` and `slide` all move boundaries around within the track without altering how much track there is. `slice` puts a cut in and changes nothing else — two items where there was one, occupying the same span. **Neighbours move.** `insert`, `ripple` and `remove` without a fill. Those are the three that shift material you did not name. **Neighbours do not move.** `overwrite`, `trim`, `slip`, `slice` and `fill`. `roll` and `slide` are the interesting pair: both change a neighbour, but only by moving the boundary they share, so nothing further along the track notices. The sample above is `insert` and `overwrite` side by side, because that is the distinction people reach for first and the one that decides whether a cut gets longer. #### Filling what an edit opens Several of these can open a hole — `overwrite` past the end of a track, `remove` with a fill, `insert` at a point past the end. Each takes a **fill template**: the item to put in that hole. Pass nothing and it is an ordinary `Gap`, which is transparent, so on a multi-track timeline whatever is underneath shows through. When you want something else — solid black, a slate, colour bars — pass a clip with a `GeneratorReference` as the template and that is what fills it instead. #### Slip and slide These two are named after the editing desk and the names are worth learning the way round. **`slip`** keeps the item where it is on the track and moves the media inside it: same four seconds of the cut, a different four seconds of the shot. The track does not change at all, and neither does any neighbour. **`slide`** keeps the media and moves the item along the track: the same four seconds of the shot, arriving earlier or later. The time has to come from somewhere, so the neighbours on either side absorb it — the one before grows or shrinks, and the one after does the opposite. #### Where they are, and where they are not All ten are in the core, in the C ABI, in every SDK generated from it, and in Python as `opentimelineio.algorithms.overwrite`, `insert` and the rest, with a `ReferencePoint` enum for `fill`'s four-point edits. Upstream's own Python package does not bind them; only its C++ has them. The Python functions here follow that C++ `editAlgorithm.h` in their names, parameters and defaults, and raise what its error handler raises. An object an edit takes out of a track stays usable while Python holds it, with no parent, as any removed child does. #### What the copied piece holds `slice`, `insert` and `overwrite` copy the piece of an item a split leaves over, and `fill` with `ReferencePoint.Sequence` copies the clip it drops in. The copy is made as upstream makes it, by the equivalent of writing the item out and reading it back, so it shares nothing with the original: an object the item holds in two places — one object under two metadata keys, one effect listed twice, one media reference under two keys — becomes two separate objects in the copy. The item left in place keeps what it held. #### An item that holds itself Metadata can hold whole objects, including the item it belongs to: `clip.metadata["self"] = clip`. `slice`, `insert` and `overwrite` copy the piece of an item a split leaves over, and `fill` copies the clip it drops in; here that copy keeps the cycle, so the leftover piece holds itself just as the original does, and the edit goes through like any other. That is a deliberate difference from upstream. Upstream copies by writing the item out and reading it back, which cannot carry a cycle, so the same edits fail there with a cycle error — and `slice`, `insert` and `overwrite` fail only after changing the track, leaving the clip cut short and the rest of it lost. The cycle still cannot be saved: writing it as `.otio` is refused, here as upstream. --- Page: https://otio-rust-docs.vercel.app/docs/guides/editing/ ### Media linkers and hooks > Pointing every clip a read produces at the right media, and running code of your own at fixed points around a read or a write. A file that comes from somewhere else rarely points at media where it lives here. An EDL names a reel, an AAF names a MobID, an `.otio` from another facility names paths on its disks. Upstream OpenTimelineIO answers that with two plugin points, and every language here has both. A **media linker** is handed each clip a read produced, and answers with the media reference that clip should use instead, or with nothing to leave it alone. A read names the linker it wants. A **hook script** is handed the whole result at a named point, and answers with what the read or write goes on with: the same object changed, or another one. Scripts are attached to a **hook**, in order, and each is handed what the one before it returned. ```c #include #include #include "otio.h" /* A cut of two clips, as an .otio file would hold it. */ static const char CUT[] = "{\"OTIO_SCHEMA\": \"Track.1\", \"name\": \"V1\", \"kind\": \"Video\"," " \"children\": [{\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"A\"}," " {\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"B\"}]}"; /* A media linker is handed each clip as it is read, and an object whose * metadata is the arguments the read was given. It answers with the media * the clip should use, made in the same document, or leaves out_result * alone to leave the clip as it was. */ static OtioStatus link_proxy(void *context, OtioDocument *document, OtioNode clip, OtioNode arguments, OtioNode *out_result, char *message, size_t message_capacity) { OtioBuffer name, root; char url[512]; (void)context; if (otio_node_name(document, clip, &name, NULL) != OTIO_STATUS_OK) { return OTIO_STATUS_INVALID_ARGUMENT; } if (otio_metadata_get_string(document, arguments, "root", &root, NULL) != OTIO_STATUS_OK) { /* Whatever goes in message is the failure the read reports. */ snprintf(message, message_capacity, "no root to link under"); otio_buffer_free(name); return OTIO_STATUS_INVALID_ARGUMENT; } snprintf(url, sizeof url, "%s/%s.mov", root.data, name.data); otio_buffer_free(root); OtioStatus status = otio_external_reference_new(document, name.data, url, out_result, NULL); otio_buffer_free(name); return status; } /* A hook script is handed the whole result, and answers with what the read * goes on with: here the same object, stamped. The context is whatever was * registered with it, here the key to stamp under. */ static OtioStatus stamp(void *context, OtioDocument *document, OtioNode target, OtioNode arguments, OtioNode *out_result, char *message, size_t message_capacity) { OtioBuffer who; (void)message; (void)message_capacity; if (otio_metadata_get_string(document, arguments, "who", &who, NULL) != OTIO_STATUS_OK) { return OTIO_STATUS_INVALID_ARGUMENT; } OtioStatus status = otio_metadata_set_string(document, target, (const char *)context, who.data, NULL); otio_buffer_free(who); *out_result = target; return status; } int main(void) { /* The last two arguments are a context handed to every call, and a * function the library calls to release it when the plugin goes. */ otio_register_media_linker("proxies", link_proxy, NULL, NULL, NULL); otio_register_hook_script("stamp", stamp, "read_by", NULL, NULL); otio_attach_hook_script("post_adapter_read", "stamp", NULL); /* The read names the linker, and carries both sets of arguments as JSON. */ OtioReadOptions options; options.struct_size = sizeof options; otio_read_options_init(&options); options.media_linker = "proxies"; options.media_linker_arguments = "{\"root\": \"/proxies\"}"; options.hook_arguments = "{\"who\": \"the conform\"}"; OtioDocument *document = NULL; OtioBuffer error; if (otio_read_from_bytes(OTIO_FORMAT_OTIO_JSON, (const uint8_t *)CUT, strlen(CUT), &options, &document, &error) != OTIO_STATUS_OK) { fprintf(stderr, "%s\n", error.data); otio_buffer_free(error); return 1; } OtioNode track, clips[2], media; OtioBuffer text; size_t count = 0; otio_document_root(document, &track, NULL); otio_metadata_get_string(document, track, "read_by", &text, NULL); printf("read by %s\n", text.data); otio_buffer_free(text); otio_node_find_clips(document, track, clips, 2, &count, NULL); for (size_t i = 0; i < count; i++) { OtioBuffer name; otio_node_name(document, clips[i], &name, NULL); otio_clip_media_reference(document, clips[i], NULL, &media, NULL); otio_external_reference_target_url(document, media, &text, NULL); printf("%s -> %s\n", name.data, text.data); otio_buffer_free(name); otio_buffer_free(text); } otio_document_free(document); otio_detach_hook_script("post_adapter_read", "stamp"); otio_unregister_hook_script("stamp"); otio_unregister_media_linker("proxies"); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/link-media-and-run-hooks/c.c #### Where they run A read parses the file, then runs, in this order, which is upstream's: | Step | Handed | | --- | --- | | `post_adapter_read` hook | the hook arguments, plus `adapter_arguments` and `media_linker_argument_map` | | the media linker, once per clip | the media linker arguments | | `post_media_linker` hook | the media linker arguments, not the hook ones, as upstream hands them | A write runs `pre_adapter_write` before it writes and `post_adapter_write` after. Writing to a file adds the path as `_filepath`. The pre-write hook works on a copy, so the object you asked to write is left exactly as it was, and what the hook returns is what gets written. Any other hook name is one of your own. Attaching a script to it declares it, and `run_hook` on an object runs it there and then. #### Choosing a linker A read with no linker named uses the one the `OTIO_DEFAULT_MEDIA_LINKER` environment variable names, or none, as upstream does. Naming one that was never registered fails the read rather than skipping the step, because a conform that silently did not link is worse than one that stopped. Asking for no linking at all (`do_not_link_media`, upstream's `MediaLinkingPolicy.DoNotLinkMedia`) leaves every clip alone, and the hooks still run. #### Arguments Each read or write can carry two argument maps: one for the linker and one for the hooks. Outside Rust and Python they cross as JSON text, an object whose values may be anything OTIO metadata can hold, timeline objects included. Your function receives them as metadata it can read with the metadata calls it already knows. A timeline object among the arguments is moved into the document the plugin works on; one the plugin puts into the timeline stays there, and the rest are removed once it has run. Arguments that are not a JSON object fail the read or write, whether or not any plugin is registered to take them. #### Failures A linker or script that fails stops the read or write, and the error carries its own message, prefixed with the plugin's name. In every language a failure is whatever that language fails with, an error, an exception, a panic, and it is turned into the library's `PLUGIN_ERROR` at the boundary rather than crossing into code that cannot handle it. #### Registering Plugins live in one registry per process, as upstream's do. Registering a name again replaces what it named; unregistering says whether there was anything to remove. A script that is unregistered while a hook still lists it fails that hook when it next runs, as upstream fails one its manifest names and cannot find, so detach it as well. Python finds plugins the way upstream does, through plugin manifests that name `.py` modules, and also sees every linker and script registered from any other language in the same process. Every other language registers functions in code, which is how a program in that language would load them anyway. [ADR 0008](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0008-native-linkers-and-hooks.md) and [ADR 0009](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0009-linkers-and-hooks-across-the-c-abi.md) say why. --- Page: https://otio-rust-docs.vercel.app/docs/guides/media-linkers-and-hooks/ ## Formats ### EDL (CMX 3600) > Reading and writing Edit Decision Lists: the rate you must supply, the dialects, and what writing refuses. An Edit Decision List is the oldest interchange format in post and the most widely understood: a title, then a numbered list of events, each one or two lines of timecode with free-form comments beneath. A file reads as a `Timeline`. ```c #include #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/read-an-edl/c.c #### An EDL does not know its own rate This is the one thing to get right. A CMX 3600 file is timecode and nothing else: it never states the rate that timecode is at, and nothing can infer it from the contents. The rate you pass is believed. Pass the wrong one and the file still reads — every event simply lands somewhere it should not. There is a usual guess, 24, and it is a guess rather than a safe default. #### What becomes what | EDL | Document | | --- | --- | | `TITLE:` | the timeline's name | | each channel the events name (`V`, `A1`, `AA/V`) | a track | | an event | a clip on every track its channel maps to | | a hole in the record timecode | a gap | | `D` and `W###` edits | a `Transition` | | `* FROM CLIP NAME:` | the clip's name | | `* FROM CLIP:` / `* FROM FILE:` / `* OTIO REFERENCE FROM:` | the clip's media | | a path with a `[1001-1020]` range in it | an `ImageSequenceReference` | | `* LOC:` | a `Marker`, with its colour | | `*ASC_SOP` and `*ASC_SAT` | the clip's `metadata["cdl"]` | | `M2` | a `LinearTimeWarp` | | a freeze-frame comment | a `FreezeFrame` | | `BL`, `BLACK`, `BARS` reels | a `GeneratorReference` | | everything else | the clip's `metadata["cmx_3600"]` | Keeping the unrecognized comments is what lets a file survive a round trip: reading each of upstream's samples, writing it back and reading it again gives the same timeline. #### Dialects The three systems that read EDLs disagree about the comment that names a clip's media, so the writer takes a style: | Style | Names media with | | --- | --- | | Avid (the default) | `* FROM CLIP:` | | Nucoda | `* FROM FILE:` | | Premiere | nothing, with the path in `* OTIO REFERENCE FROM:` | Premiere reads any `FROM` comment as meaning the clip has no name and calls it `UNKNOWN`, so that dialect writes `AX` as every reel and puts the path in a comment Premiere ignores and this adapter can read back. From Rust the style is an enum, so an unknown dialect cannot be spelled at all; from Python it is upstream's `style` keyword argument. #### Reel names Most systems will only read a reel name of eight characters, so the writer pads or truncates to that by default. A truncated name is recorded in an `* OTIO TRUNCATED REEL NAME FROM:` comment, so reading the file back gets the original. Asking for no limit writes the name in full, which loses nothing but which most systems will not read. #### What writing will not do Writing is narrower than reading, as it is upstream: - Only one enabled video track, and at most two audio tracks. An EDL describes a single strand of picture, so a timeline with two video tracks has no EDL form and is refused. - Dissolves, but not wipes. A wipe reads, and writes back out as a dissolve. - One timing effect per clip, and only a speed change or a freeze frame. #### Where it differs from upstream The adapter is a port of upstream's `otio-cmx3600-adapter` and reproduces its behaviour, with two differences: - The writer works on a copy of the document, so writing does not change what you handed it. Upstream rewrites the timeline in place, and a caller who writes twice gets different text the second time. - A marker's colour is a real colour here rather than the string `"RED"`, so the name is canonicalized to `"Red"` and given its components. The name the file used is still on the marker's `metadata["cmx_3600"]`. The [crate's README](https://github.com/alchemist-editor/otio-rust/blob/main/crates/otio-cmx3600/README.md) has the Rust options in full. --- Page: https://otio-rust-docs.vercel.app/docs/formats/edl/ ### ALE (Avid Log Exchange) > Reading and writing shot logs, and where each column lands on a clip. An ALE is a tab-separated shot log: a `Heading` of key/value pairs, a `Column` line naming the fields, and a `Data` section with one row per clip. It is a list of clips rather than a cut, so a file reads as a `SerializableCollection` of clips, not a timeline. #### What becomes what | ALE | Document | | --- | --- | | `Heading` pairs | the collection's `metadata["ALE"]["header"]` | | `Column` order | the collection's `metadata["ALE"]["columns"]` | | `Name` | the clip's name (and stays as a column) | | `Start`, `Duration`, `End` | the clip's `source_range` | | `Source File` | an `ExternalReference` on the clip | | `CDL`, `ASC_SOP`, `ASC_SAT` | the clip's `metadata["cdl"]` | | every other column | the clip's `metadata["ALE"]` | Keeping the unrecognized columns and the column order is what makes a round trip lossless: upstream's own `sample.ale` reads and writes back byte for byte. #### Rates Unlike an EDL, an ALE states its rate, in the heading's `FPS`, and that rate wins over the one you pass, because the file was written for it. Timecode exists only at SMPTE rates, so a heading saying `23.976` is read at 24000/1001; the heading keeps the spelling it arrived with. A rate that is not near a SMPTE rate is refused rather than quietly rounded. #### Quirks kept from upstream - A value containing a tab is written as-is and becomes two columns when read back. Upstream means to replace tabs with spaces and throws the result away. - The `Name` column stays in `metadata["ALE"]` as well as becoming the clip's name, so it is in the document twice. The writer depends on this: it discovers columns from that metadata. #### Where it differs from upstream - A doubled sign such as `--1.0` in an `ASC_SOP` column simply does not start a number. Upstream matches it and then fails to convert, failing the whole row. - Reading moves `ASC_SOP`, `ASC_SAT` and `CDL` into `metadata["cdl"]`, and the writer rebuilds those columns from there. Upstream's writer looks only at the `ALE` metadata, so a graded file it has just read writes those columns blank. The numbers go back out through a float, so `-0.0870` is written as `-0.087`. - The writer snaps the heading's rate to a SMPTE rate the same way the reader does. Upstream writes at the stated decimal, so a file at `23.976` comes back with every time slid by a couple of frames. The [crate's README](https://github.com/alchemist-editor/otio-rust/blob/main/crates/otio-ale/README.md) has the Rust options. --- Page: https://otio-rust-docs.vercel.app/docs/formats/ale/ ### Final Cut Pro XML > FCP 7 interchange XML and FCP X XML: what each reads as, and what is kept for the round trip. Two different formats share the name. FCP 7's `.xml` is the older one, and still how a great many tools hand an edit to one another; FCP X's `.fcpxml` is Final Cut's current format, and it does not think in tracks. Both are ports of upstream's adapters, and both keep what OTIO has no field for in metadata so a file read and written again keeps it. ```c #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/convert-a-format/c.c #### Final Cut Pro 7 XML FCP 7 itself is long gone, but its XML is not: Premiere Pro, Resolve, Hiero and Media Composer all read or write some dialect of it. The format carries far more per-element detail than OTIO has fields for. Everything the adapter does not turn into a real OTIO field is kept under the `fcp_xml` key in the relevant object's metadata and written back out on the way past, so a file read and written again keeps its colour settings, its effect parameters and its host application's bookkeeping. One fix to plain reading departs from upstream: a nested sequence reads with its tracks, where upstream reads every one as an empty stack. #### Final Cut Pro X XML Final Cut does not think in tracks. A sequence holds one `spine`, the main storyline, and everything layered over or under it hangs off whichever storyline item it overlaps, carrying a `lane` number saying how far above or below it sits. Reading works out where each element really starts and groups the results by lane, one track per lane. Writing puts lane zero back into the spine and reattaches the rest. What comes back from a read depends on what the file holds: | The file holds | It reads as | | --- | --- | | a library or an event | a `SerializableCollection` of timelines | | a bare project | a `Timeline` | | loose clips | a collection of clips and compound clips | What Final Cut knows about a piece of media that OTIO has no field for (its note, its keywords and its Spotlight metadata) is kept under the `fcpx` key in the media reference's metadata and written back out on the way past. #### Conversions upstream cannot do Several conversions that fail outright with upstream's adapters work here, each a documented departure on the adapter it concerns: - An EDL whose tracks start anywhere but `00:00:00:00`, or an audio-only EDL, written as FCP X XML. - An FCP 7 timeline with a 15 fps clip, written as FCP X XML. - FCP X XML written as FCP 7 XML, which upstream wrote as a file that could not be read back. --- Page: https://otio-rust-docs.vercel.app/docs/formats/final-cut-pro/ ### AAF > Reading, writing and changing AAF files, the options each takes, and how a written file relinks in Media Composer. AAF is how a cut reaches Avid Media Composer. Reading and writing are ports of upstream's AAF adapter, checked byte for byte against what upstream reads and writes, and AAF is available from every language. #### Reading an AAF An AAF reads as upstream's adapter reads it, byte for byte once written as OTIO JSON, on every sample file in upstream's test data. Upstream's four reading options are all here, with upstream's defaults: - `simplify` (on) collapses the nesting AAF has and OTIO does not need. - `attach_markers` (on) moves each marker from the slot that carries it onto the item it points at. - `bake_keyframed_properties` (off) records each keyframed effect parameter's value at every frame of its effect as `keyframe_baked_values`, interpolated the way pyaaf2 interpolates it. - `transcribe_log` (off) prints a line for each thing the reader makes, word for word what upstream prints. From Rust it takes a `TranscribeLog`, which hands each line to a function of your choosing. It is not in the C ABI or the SDKs built on it, since it would need a callback across it. From C and the SDKs, the first two are spelled as what turning them off does, `aaf_keep_nesting` and `aaf_markers_on_slots`, so that options left at zero read an AAF the way upstream does. The third is `aaf_bake_keyframes`. Baked curves go through the platform's `pow`, `acos` and `cos`, so off Linux the last digit of a baked value can differ from upstream's. Three log lines upstream prints for rare failures embed a Python object's memory address or a whole track, and those are worded differently here. #### Writing an AAF AAF is how a cut reaches Avid Media Composer, and writing one is a port of upstream's AAF adapter rather than a design of its own. Each track becomes a slot of a composition. Each clip becomes a chain of three objects: a master clip, a file describing the media, and a tape carrying its timecode, which is the chain Media Composer expects to relink through. Gaps, dissolves, nested tracks, stacks and markers all have an AAF form; any other transition is left out, as upstream leaves it out. ```c #include #include #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); OtioTimeRange one_second; one_second.start_time.value = 0.0; one_second.start_time.rate = 24.0; one_second.duration.value = 24.0; one_second.duration.rate = 24.0; /* An AAF clip is cut from media of a known length, so each clip's media * says how much of it there is. A new clip has no media at all, so its * reference goes in under upstream's key and is made the active one. */ const char *names[] = {"A001C003", "A001C004"}; for (size_t index = 0; index < 2; index += 1) { char url[64]; snprintf(url, sizeof url, "file:///media/%s.mov", names[index]); OtioNode reference; otio_external_reference_new(document, NULL, url, &reference, NULL); otio_media_reference_set_available_range(document, reference, one_second, NULL); OtioNode clip; otio_clip_new(document, names[index], &clip, NULL); otio_clip_set_media_reference(document, clip, "DEFAULT_MEDIA", reference, NULL); otio_clip_set_active_media_reference_key(document, clip, "DEFAULT_MEDIA", NULL); otio_item_set_source_range(document, clip, one_second, NULL); otio_composition_append_child(document, track, clip, NULL); } otio_document_set_root(document, timeline, NULL); /* Every clip needs a MobID, from its metadata, its media's metadata or * the AAF its media names. A cut built from scratch has none, so let the * writer make them up rather than refuse the clip. */ OtioWriteOptions options; options.struct_size = sizeof options; otio_write_options_init(&options); options.aaf_use_empty_mob_ids = true; OtioBuffer error; if (otio_write_to_file(OTIO_FORMAT_AAF, document, "cut.aaf", &options, &error) != OTIO_STATUS_OK) { fprintf(stderr, "%s\n", error.data); otio_buffer_free(error); return 1; } otio_document_free(document); OtioDocument *written = NULL; if (otio_read_from_file(OTIO_FORMAT_AAF, "cut.aaf", NULL, &written, &error) != OTIO_STATUS_OK) { fprintf(stderr, "%s\n", error.data); otio_buffer_free(error); return 1; } OtioNode root; otio_document_root(written, &root, NULL); size_t count = 0; otio_node_find_clips(written, root, NULL, 0, &count, NULL); OtioNode *clips = malloc(count * sizeof *clips); otio_node_find_clips(written, root, clips, count, &count, NULL); for (size_t index = 0; index < count; index += 1) { OtioBuffer name; if (otio_node_name(written, clips[index], &name, NULL) == OTIO_STATUS_OK) { printf("%s\n", name.data); otio_buffer_free(name); } } free(clips); otio_document_free(written); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/c.c A clip is tied to its media by a MobID. A cut read from an AAF already has one on every clip, kept under `metadata["AAF"]`, and it is written back, so the file relinks to the media it came from. A clip whose media is itself an AAF holding one master clip takes that clip's MobID. Anything else, such as a cut built from scratch, has none, and writing it needs `use_empty_mob_ids`, which makes MobIDs up. A made-up MobID links to no media Media Composer knows, which is why upstream refuses a clip without one by default, and so does this. Before writing anything, the writer checks the whole timeline the way upstream does: every item at the timeline's rate, every clip saying how much media it has, and every transition carrying what an AAF dissolve is built from. It reports everything it finds wrong at once rather than stopping at the first. The file is the same, byte for byte, as the one upstream's adapter writes from the same timeline, given the same clock and the same random identifiers. The tests check that on twelve files, and it holds on all 33 samples in upstream's own test data that upstream can write. That parity is the evidence the file suits Media Composer: none of the files has been imported into Media Composer as part of testing. Embedding the media in the file works as upstream's `embed_essence` does. Each clip's media URL is taken as a path, relative to the working directory unless it is absolute. An `.aaf` there has the master mob with the clip's MobID copied out of it, with its source mob and essence. A `.dnx` on a video track is imported as a raw DNxHD stream. Anything else is refused as upstream refuses it, and that includes a WAV file, which upstream sends to the DNxHD import too. From Python, upstream's four AAF hooks run where upstream runs them: `otio_aaf_pre_read_transcribe` and `otio_aaf_post_read_transcribe` before and after the file is transcribed, and `otio_aaf_pre_write_transcribe` and `otio_aaf_post_write_transcribe` before and after it is written. A `pre_write` hook is where to transcode other media into something `embed_essence` can embed, as upstream suggests: what it returns is what is written. Each hook is handed upstream's arguments, except that `aaf_handle`, upstream's open pyaaf2 file, is `None`, because there is no pyaaf2 file here. From Rust, `ReadOptions::with_post_transcribe` runs a callback at the one point inside the read; the other three run around the read or write, so a caller runs them itself. AAF writing is available from every language. From Python, `otio.adapters.write_to_file(timeline, "cut.aaf")` takes upstream's keyword arguments, `embed_essence=True` among them. From C and the SDKs, the same options are `aaf_`-prefixed fields of the write options, along with three a library caller needs and a Python one does not: `aaf_user`, whom a new marker is credited to when no login name is set; `aaf_time`, the time the file records; and `aaf_id_seed`, which seeds the identifiers it makes up. The same time, seed and timeline write the same file. WebAssembly has no clock or randomness of its own, so the TypeScript package passes the time and a fresh seed on every write unless you give your own. Nor has it a file system, so it cannot embed media: with `aafEmbedEssence` set, a clip whose media names a file stops the write, as the file cannot be found. #### Changing an existing AAF Below the adapter, the `aaf` crate can open an AAF file that already exists, change it and save it, as pyaaf2 does when it opens a file with `'r+'`. The same edits in the same order leave the same bytes pyaaf2 leaves, which twenty scenarios in its tests check against files pyaaf2 changed: properties changed, added and removed, mobs and slots added and taken out, new definitions and classes, and essence moved and dropped. It works on the AAF object model, not on a timeline, and is available from Rust only. The [crate's README](https://github.com/alchemist-editor/otio-rust/blob/main/crates/aaf/README.md#changing-a-file) has an example, and [ADR 0005](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0005-aaf-modify-path.md) the design. --- Page: https://otio-rust-docs.vercel.app/docs/formats/aaf/ ### Bundles (.otioz and .otiod) > Packaging a timeline with its media, and what happens to media that is not a file. A bundle is how a cut travels with its media: the timeline and every file it references, in one archive or one directory, so it can be moved to another machine and opened there. ```text cut.otioz / cut.otiod ├── version.txt "1.0.0" ├── content.otio the timeline, references rewritten to media/... └── media/ ├── shot_010.mov └── render.0001.exr ... ``` An `.otioz` is a zip archive, with `content.otio` deflated and the media stored uncompressed so it can be read in place; an `.otiod` is the same layout as a directory. The `otio-bundle` crate ports upstream's `bundle.cpp`. #### Writing a bundle A bundle is how a cut travels with its media. Writing one copies every file the timeline's media references name into `media/` and points each reference at its copy, so the bundle can be moved to another machine and opened there. Every file lands directly under `media/`, so two media files with the same name in different directories cannot both go in, and the write fails rather than dropping one. Nor is a bundle ever written over: a path that already exists stops the write. ```c #include #include "otio.h" int main(void) { OtioDocument *document = otio_document_new(); OtioNode timeline, stack, track; otio_timeline_new(document, "Cut", &timeline, NULL); otio_timeline_tracks(document, timeline, &stack, NULL); otio_track_new(document, "V1", "Video", &track, NULL); otio_composition_append_child(document, stack, track, NULL); /* A cut of two clips: one whose media is a file beside the program, and * one whose media is on the web. */ const char *names[] = {"A001C003", "A001C004"}; const char *urls[] = {"shot.mov", "https://example.com/remote.mov"}; for (size_t index = 0; index < 2; index += 1) { OtioNode reference, clip; otio_external_reference_new(document, NULL, urls[index], &reference, NULL); otio_clip_new(document, names[index], &clip, NULL); otio_clip_set_media_reference(document, clip, "DEFAULT_MEDIA", reference, NULL); otio_clip_set_active_media_reference_key(document, clip, "DEFAULT_MEDIA", NULL); otio_composition_append_child(document, track, clip, NULL); } /* A bundle holds a timeline, and what is written is the document's root. */ otio_document_set_root(document, timeline, NULL); /* Every clip whose media is a file has the file copied into the bundle * and its reference pointed at the copy. Media that is not a file would * stop the write, so it is made missing instead. OTIO_FORMAT_OTIOD * writes the same layout as a directory. */ OtioWriteOptions options; options.struct_size = sizeof options; otio_write_options_init(&options); options.bundle_media_policy = OTIO_BUNDLE_MEDIA_POLICY_MISSING_IF_NOT_FILE; OtioBuffer error; if (otio_write_to_file(OTIO_FORMAT_OTIOZ, document, "cut.otioz", &options, &error) != OTIO_STATUS_OK) { fprintf(stderr, "%s\n", error.data); otio_buffer_free(error); return 1; } otio_document_free(document); /* Unpacked, with each reference made absolute, the media is ready to * use. */ OtioReadOptions read; read.struct_size = sizeof read; otio_read_options_init(&read); read.bundle_extract_path = "cut"; read.bundle_absolute_media_paths = true; OtioDocument *bundled = NULL; if (otio_read_from_file(OTIO_FORMAT_OTIOZ, "cut.otioz", &read, &bundled, &error) != OTIO_STATUS_OK) { fprintf(stderr, "%s\n", error.data); otio_buffer_free(error); return 1; } OtioNode root, clips[2]; size_t count = 0; otio_document_root(bundled, &root, NULL); otio_node_find_clips(bundled, root, clips, 2, &count, NULL); for (size_t index = 0; index < count; index += 1) { OtioNode media; OtioNodeKind kind; OtioBuffer url; otio_clip_media_reference(bundled, clips[index], NULL, &media, NULL); otio_node_kind(bundled, media, &kind, NULL); if (kind == OTIO_NODE_KIND_EXTERNAL_REFERENCE && otio_external_reference_target_url(bundled, media, &url, NULL) == OTIO_STATUS_OK) { printf("%s\n", url.data); otio_buffer_free(url); } else { printf("missing\n"); } } otio_document_free(bundled); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-a-bundle/c.c Media that is not a file on disk, such as a URL on the web or a generator, is what upstream's media reference policy is for. By default it stops the write; told to, the writer replaces each such reference with a missing reference instead, or replaces every reference and bundles no media at all. A relative media path is found from the working directory unless you name another. Reading an `.otioz` reads only the timeline out of the archive unless you ask for it to be unpacked into a directory, which must not exist yet; either bundle can then have its references rewritten as absolute paths into it. From Rust these are `otio_bundle::WriteOptions` and `ReadOptions`, and from Python upstream's `media_policy`, `relative_media_base_dir` and `extract_to_directory` keyword arguments. From C and the SDKs, the bundle formats are `OTIO_FORMAT_OTIOZ` and `OTIO_FORMAT_OTIOD`, and the options are fields of the read and write options: `bundle_media_policy` and `bundle_media_base_dir` for writing, `bundle_extract_path` and `bundle_absolute_media_paths` for reading. A bundle is read and written through a path, never as bytes, and the calls that take bytes refuse one. The TypeScript package has no file system to keep a bundle on, so it has no bundle formats at all. #### Media URLs with unusual characters Media URLs are percent-decoded exactly as upstream decodes them, so a URL with a `%` that is not followed by a hex digit, such as `a%zz.mov`, fails the write (`ValueError("stoi")` from Python) whatever the policy, as it does upstream. An escape that spells a byte that is not UTF-8, such as `%E9` in `caf%E9.mov`, names a file whose name has that raw byte, and on Linux (or any Unix filesystem that allows such a name) that file is found and bundled. Inside the bundle it is named `caf%E9.mov`, with the escape spelled out, because upstream's raw name makes `content.otio` invalid JSON. From Python, `url_utils.filepath_from_url` on such a URL raises `UnicodeDecodeError`, as upstream's does. The [crate's README](https://github.com/alchemist-editor/otio-rust/blob/main/crates/otio-bundle/README.md) covers the zip implementation and the Rust options. --- Page: https://otio-rust-docs.vercel.app/docs/formats/bundles/ ## Concepts ### 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. ```c #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/time-math/c.c 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. ```c #include #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; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/c.c #### 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/ ### Timeline structure > How stacks, tracks, clips, gaps and transitions nest, and what a flattened timeline looks like. A timeline holds tracks, and tracks hold clips — and then somebody nests a stack inside a track and it stops being obvious. This page is the shapes: what contains what, and what the result means when something plays it. Upstream's figures are pictures; these are the same structures as text, which is the same information and survives being copied into a terminal. #### A simple cut list The smallest useful timeline is a single track of clips laid end to end. ```text Timeline └── Stack "tracks" └── Track "Track-001" ├── Clip "Clip-001" source_range 0+10 → Media-001 available 0+20 ├── Clip "Clip-002" source_range 5+10 → Media-002 available 0+15 ├── Clip "Clip-003" source_range 2+8 → Media-003 available 100+30 └── Clip "Clip-004" source_range none → Media-004 available 0+12 ``` A timeline always has that top-level stack, even with one track in it, because a timeline can hold several and something has to lay them over each other. At the bottom, each media reference has a `target_url` saying where the media is and an `available_range` saying how much of it there is. Ranges are written here as `start+duration` in frames; `Media-003` starting at 100 means its first frame is timecode 00:00:04:04 at 24fps, not that it is offset in the cut. Above them, each clip's `source_range` picks the piece it uses. `Clip-004` has none, so it uses all of `Media-004` — and asking any of them for `trimmed_range` answers without the caller having to know which case it is in. A `source_range` may point outside the `available_range`. That happens for real — only the first half of a shot has been rendered — and nothing here snaps it back or complains. What a player does about it is the player's business. The track's own length is the sum of its children's trimmed ranges. Nobody sets it. #### Transitions A transition blends two neighbours on the same track: a dissolve, a wipe. It can sit between any two composable items, not only clips. ```text Track "Track-001" ├── Clip "Clip-002" ends at frame 10 ├── Transition in_offset 2, out_offset 3 └── Clip "Clip-003" starts at frame 0 ``` `out_offset` reaches back into the clip before it, `in_offset` forward into the clip after. So `Clip-002` is on screen for three frames longer than its own range says, and `Clip-003` for two frames earlier — which is exactly what `visible_range` reports for each while `trimmed_range` does not. See [Time ranges](https://otio-rust-docs.vercel.app/docs/concepts/time-ranges/) for the difference. **A transition changes no length.** The track is as long with it as without, so a tool that cannot render one may ignore it and still agree about where everything is. The format expects that a transition's offsets stay inside its neighbours, that a clip with transitions at both ends has them not overlapping, and that two transitions are never adjacent. Nothing in this library enforces any of that — upstream does not either — so a file can carry all three and will be read back as written. A fade to or from black is usually a transition against a `Gap`, which may have zero duration. A gap is *transparent* rather than black, so with tracks underneath it the thing below shows through; for real black, use a clip with a `GeneratorReference`. #### Several tracks Tracks in a stack play at the same time, lower entries first. ```text Stack "tracks" ├── Track-001 [ Clip-001 ][ Gap 4 ][ Clip-002 ] ├── Track-002 [ Gap ][ Clip-003 ][ Clip-005 ] └── Track-003 [ Clip-006 ] (trimmed by its own source_range) flattened [ Clip-001 ][Clip-003][ Clip-002 ] ``` Where `Track-001` has a gap, the track below shows through — those four frames of `Clip-003` are what the flattened result carries. `Clip-005` is covered by `Clip-003` the whole time, so it does not appear at all. The gap at the head of `Track-002` is doing nothing but offsetting what follows it. That is the usual way to shift a track's contents; setting the track's own `source_range` is the other, and `Track-003` does it that way. Tracks in a stack need not be the same length. Making a short one match means appending a gap, or extending its `source_range` — either way `trimmed_range` ends up the same, which is the only thing downstream cares about. Image tracks composite in painter order, bottom to top, over a background that is zero in colour and full in alpha. Audio tracks sum. Neither is something OTIO does — it is what OTIO *means*, and the application does it. What an effect on a clip does is not specified at all. #### Nesting A track's children can be any composable item, and that includes other tracks and stacks. ```text Track "Track-001" ├── Clip "Clip-001" ├── Stack "Nested Stack" source_range 2+6 │ ├── Track-A [ Clip-003 ] │ └── Track-B [ Clip-005 ] (trimmed by its own source_range) ├── Gap └── Clip "Clip-004" ``` A nested composition behaves exactly like a clip that happens to have complicated contents instead of a media reference. Give it a `source_range` and only that slice of it is included — frames 2 to 7 here. Give it none and its whole `available_range` is computed and used. The consequence worth remembering: **nesting is opaque**. The gap in `Track-001` cannot see `Clip-003`, because `Clip-003` is not its peer — it is inside `Nested Stack`, and contents do not spill out into the neighbouring gaps of the outer track. --- This page is adapted from [Timeline Structure](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/docs/tutorials/otio-timeline-structure.md) in upstream OpenTimelineIO's documentation, which is Apache-2.0 like this project. Its figures have been redrawn as text, and the claims about what is and is not enforced were checked against this implementation. --- Page: https://otio-rust-docs.vercel.app/docs/concepts/timeline-structure/ ### Time ranges > The five ranges an item has, which clock each one answers in, and why they differ. A timeline is a one-dimensional coordinate system, and laying clips end-to-end in a track is the easy case. Trimming, nesting, dissolves and speed changes are where it stops being obvious, and the confusion is almost always the same one: two different ranges were read as though they were measured against the same clock. They are not. Every range below belongs either to the **item's own clock** — the media's frame numbering — or to its **parent's clock**, the track it sits in. Getting this right is most of getting OTIO right. ```c #include #include "otio.h" static void print_range(const char *label, OtioTimeRange span) { printf("%s %g for %g\n", label, span.start_time.value, span.duration.value); } int main(void) { OtioDocument *document = otio_document_new(); /* Ten seconds of rushes on disk. The available range belongs to the * media, not to the clip: it is what the file offers, whoever uses it. */ OtioNode media; otio_external_reference_new(document, "A001", "file:///A001.mov", &media, NULL); OtioTimeRange available; available.start_time.value = 0.0; available.start_time.rate = 24.0; available.duration.value = 240.0; available.duration.rate = 24.0; otio_media_reference_set_available_range(document, media, available, NULL); /* Three seconds of it, starting two seconds in. A source range is in the * media's clock, which is why it starts at 48 rather than at 0. */ OtioNode clip; otio_clip_new(document, "shot", &clip, NULL); otio_clip_set_media_reference(document, clip, "DEFAULT_MEDIA", media, NULL); OtioTimeRange source; source.start_time.value = 48.0; source.start_time.rate = 24.0; source.duration.value = 72.0; source.duration.rate = 24.0; otio_item_set_source_range(document, clip, source, NULL); /* A second of black in front of it, so the clip does not start the * track. */ OtioNode head; otio_gap_new(document, NULL, &head, NULL); OtioTimeRange black; black.start_time.value = 0.0; black.start_time.rate = 24.0; black.duration.value = 24.0; black.duration.rate = 24.0; otio_item_set_source_range(document, head, black, NULL); OtioNode track; otio_track_new(document, "V1", "Video", &track, NULL); otio_composition_append_child(document, track, head, NULL); otio_composition_append_child(document, track, clip, NULL); /* The same clip, asked four questions. The first three answer in the * media's clock; the last answers in the track's. */ OtioTimeRange range; otio_item_available_range(document, clip, &range, NULL); print_range("available:", range); otio_item_trimmed_range(document, clip, &range, NULL); print_range("trimmed: ", range); otio_item_visible_range(document, clip, &range, NULL); print_range("visible: ", range); otio_item_range_in_parent(document, clip, &range, NULL); print_range("in parent:", range); otio_document_free(document); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/c.c That sample prints four different answers about one clip, and the rest of this page is why. #### The ranges in the item's own clock ##### `available_range` Everything behind the item, before anything trims it. For a clip that is the active media reference's own `available_range`: if the clip points at `wedding.mov`, this is how long that file is and what timecode it starts at. For a track it is the sum of its children; for a stack, the longest of them. It can be unknown. Media goes missing, and a URL can be expensive enough to query that nobody has. Upstream answers `None` in that case; here asking a clip whose media says nothing about its length is an error rather than an answer — `NoAvailableRange` in Rust, which reaches C as `OTIO_STATUS_CORE_ERROR` and each SDK as whatever that SDK makes of a failure. Note the difference from `source_range` below, where absence *is* one of the answers and the status is `OTIO_STATUS_NO_VALUE`. ##### `source_range` The piece of the media this item uses — the trim, and the only one of these ranges you set rather than ask for. It may be absent, which means the item uses all of its media. An item with neither a `source_range` nor an `available_range` is invalid: something has to say how long it is. It is usually shorter than what is available, but it does not have to be. Asking for a range that runs past the end of the media is a real thing to do — "this shot needs to be four seconds and only two have been rendered" — and neither upstream nor this port will stop you or snap it back. What a player does about it is the player's business. ##### `trimmed_range` The `source_range` if there is one, and the `available_range` if there is not. This is how long the item is as far as its parent is concerned, and it is what `duration` reports. ##### `visible_range` The `trimmed_range`, widened by any transition that reaches into the item. A clip trimmed to end at frame 10, followed by a dissolve with an `out_offset` of 5, is on screen until frame 15 — so its `visible_range` ends at 15 while its `trimmed_range` ends at 10. With no transition touching it, the two are equal, which is why the sample above prints the same numbers for both. #### The ranges in the parent's clock ##### `range_in_parent` Where the item sits in the composition holding it. In a **track** this is what you would expect, and one invariant follows from it: each child begins exactly where the previous one ended, so for adjacent clips A and B in a track, ```text A.range_in_parent().end_time_exclusive() == B.range_in_parent().start_time() ``` In a **stack** it is duller, because a stack lays its children over the same span: the start is always zero and the duration is the child's own. That is what "these tracks are on top of each other" means. To offset something, put it in a track and put a gap in front of it — or trim the track itself. ##### `trimmed_range_in_parent` The same, clipped to the parent's own `source_range`. Most parents have no `source_range`, so most of the time this is `range_in_parent` again. When the parent *is* trimmed, this is the honest answer about where the child is relative to what survives the trim, and when the parent's trim excludes the child entirely there is no answer at all: `None` in Rust, `OTIO_STATUS_NO_VALUE` through the C ABI. One inherited quirk: a *direct* child trimmed out of its composition raises `InvalidTimeRange` rather than answering "nothing". That is upstream's behaviour and it is deliberate here, because the alternative — handing back a zero-length range — would quietly place the item at the head of the track. #### Compositions A track or a stack is an item too, so it has all of the above. Two more are worth naming: - **`range_of_child_at_index`** — where the nth child sits, transitions included. - **`trimmed_range_of_child_at_index`** — the same, relative to the composition's own trim. Asking a track or a stack for its `duration` sums its children, which is why nobody has to tell a track how long it is. #### Markers A marker can hang off any item, and its `marked_range` is in **that item's** clock — the same clock as the item's `source_range`. A marker on a clip is positioned in the media's numbering; a marker on a track is positioned along the track. Its duration may be zero, which means an instant rather than a span. Both are ordinary. #### Transitions A transition has `in_offset` and `out_offset` rather than a range of its own: how far it reaches back into the item before it and forward into the item after. `range_in_parent` and `trimmed_range_in_parent` work on one, and both defer to the parent. A transition does not make its track any longer or shorter. A tool that cannot render one can ignore it and still get the timeline's length right. --- This page is adapted from [Time Ranges](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/docs/tutorials/time-ranges.md) in upstream OpenTimelineIO's documentation, which is Apache-2.0 like this project. The behaviour described has been checked against this implementation rather than carried over on trust; where the two differ — the shape of "there is no answer", chiefly — it says so above. --- Page: https://otio-rust-docs.vercel.app/docs/concepts/time-ranges/ ### 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: ```json { "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. ```text 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](https://otio-rust-docs.vercel.app/docs/concepts/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: ```json "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](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/docs/tutorials/otio-file-format-specification.md) in upstream OpenTimelineIO's documentation, which is Apache-2.0 like this project. The guarantees section describes this implementation and has no counterpart upstream. --- Page: https://otio-rust-docs.vercel.app/docs/concepts/otio-file-format/ ## Extensions ### Extensions > Work built on otio-rust's core that is not OpenTimelineIO, kept apart from it and off until a program turns it on. Everything else on this site is OpenTimelineIO: the object model, the time library, the file formats, ported from upstream and checked against upstream's own tests. An **extension** is something more, built in the same repository on the same core, that upstream OpenTimelineIO does not have. Extensions are kept apart so nobody mistakes one for OpenTimelineIO: - **Off until enabled.** An extension does nothing until a program turns it on. While it is off, any schema it owns reads as an unknown schema and is written back unchanged, as upstream OpenTimelineIO treats a schema whose plugin is not loaded. A file written with an extension on still opens in upstream's tools. - **A module of its own in every language.** Importing OpenTimelineIO never pulls an extension in. You import the extension's module separately. - **Its own C header.** The functions are in the same `libotio`, prefixed `otio_ext__`, and declared in `otio_ext_.h` beside `otio.h`. | Extension | What it does | | --- | --- | | [Timeline sync](https://otio-rust-docs.vercel.app/docs/extensions/timeline-sync/) | Shares a timeline between programs while it is edited | | [ORI Annotations](https://otio-rust-docs.vercel.app/docs/extensions/ori-annotations/) | Reads, writes and edits the ORI project's review annotations | #### Turning one on Every language has the same two calls on the core, by extension id, and each extension's module has its own `enable` and `is_enabled`: | Language | Core | Sync's module | | --- | --- | --- | | Rust | `otio_ext::enable("sync")` | `otio_ext_sync::install()`, then `otio_ext::enable(otio_ext_sync::ID)` | | Python | `opentimelineio.ext.enable("sync")` | `from opentimelineio.ext import sync; sync.enable()` | | C | `otio_ext_enable("sync", &error)` | `otio_ext_sync_enable(&error)`, from `otio_ext_sync.h` | Enabling an extension that is already on does nothing. Enabling one turns on the extensions it depends on first. Enabling is for the whole process and cannot be undone, as registering a schema cannot be. --- Page: https://otio-rust-docs.vercel.app/docs/extensions/overview/ ### Timeline sync > Sharing an OpenTimelineIO timeline between programs while it is edited, over Alchemist's Furnace or ORI Sync. Timeline sync keeps one timeline the same in several programs while people edit it: an editor in the browser, a reviewer on an iPad, a desktop editor, RV or xStudio. It is otio-rust's first extension. Two systems already do this. ORI Sync, from the ORI Annotations project, runs between OpenRV and xStudio over RabbitMQ. Alchemist's Furnace is a WebSocket server between Alchemist's clients. Both name every object with a GUID in `metadata.sync.guid` and send changes as patches addressed by GUID. Timeline sync is one implementation of that model, written once in Rust and reached from every language, that speaks both systems' wire formats. #### Where it stands Sync is being built in phases, tracked in [epic #115](https://github.com/alchemist-editor/otio-rust/issues/115): 1. **The extension itself**: sync can be enabled, and every SDK has its module. 2. **Identity and patches**: GUIDs, patches made by transactions or by comparing two versions of a timeline, and applying them. 3. **Sessions over Furnace**: a session that takes bytes in and hands bytes out, with no networking of its own, Furnace's wire format, a WebSocket client, and compression, in Rust and, through the C interface, in every SDK. 4. **Furnace relays patches** instead of whole timelines. 5. **ORI's wire format** over RabbitMQ, for RV and xStudio, in Rust. #### Turning it on | Language | Import | Enable | | --- | --- | --- | | Rust | `otio-ext-sync` | `otio_ext_sync::install(); otio_ext::enable(otio_ext_sync::ID)?;` | | Python | `from opentimelineio.ext import sync` | `sync.enable()` | | C | `#include "otio_ext_sync.h"` | `otio_ext_sync_enable(&error);` | | Go | `github.com/alchemist-editor/otio-rust/sdk/go/ext/sync` | `sync.Enable()` | | Swift | `import OTIOExtSync` | `try Sync.enable()` | | TypeScript | `import * as sync from "@alchemist-edit/otio/ext/sync"` | `await init(); sync.enable();` | | C++ | `opentimelineio/ext/sync.hpp` | `otio::ext::sync::enable();` | | C# | `using OpenTimelineIO.Ext.Sync;` | `SyncExtension.Enable();` | | Objective-C | `#import ` | `OTIOExtSyncEnable(&error);` | | Zig | `otio.ext.sync` | `try otio.ext.sync.enable();` | #### Identity Every object sync addresses carries a GUID in `metadata.sync.guid`: the timeline, its stack, and every track, stack and item under it. Markers, effects and media references travel inside the item that holds them. A GUID is ordinary metadata, so a file carrying them opens anywhere OpenTimelineIO does. Sync gives an object a GUID when it needs one. Beginning a transaction stamps every object that lacks one, and an object that shares a GUID with one met earlier (usually a copy made with its metadata) gets a new one; the first keeps it. You can also stamp a timeline yourself, read or set one object's GUID, make a new GUID, and find an object by its GUID. #### Patches A change to a timeline travels as a list of patches, in Alchemist's Furnace format: `insert_child`, `move_child`, `remove_child` and `set_property`, each naming the objects it is about by GUID. A change too big for patches (more than 500 of them, or more than 40% of the timeline's objects), a change between two sequences, or one that goes back a generation, is sent as one `replace_timeline` patch holding the whole timeline instead. There are two ways to get patches, and both give the same ones. **Compare two versions.** Diffing two snapshots, each a timeline with its sequence id and generation as Furnace sends it, gives the patches between them. It is a port of Furnace's own diff, checked against Furnace's golden files in Rust and in every SDK, so both ends agree byte for byte. ```c #include #include "otio_ext_sync.h" /* Two versions of one sequence, as Furnace sends them: in the second, clip B * has moved in front of clip A. Every object carries its GUID in * metadata.sync.guid, which is how a patch names it. */ #define CLIP(name, guid) \ "{\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"" name "\"," \ " \"metadata\": {\"sync\": {\"guid\": \"" guid "\"}}}" #define SNAPSHOT(generation, first, second) \ "{\"sequenceId\": \"main\", \"generation\": " generation ", \"otioJson\": {" \ "\"OTIO_SCHEMA\": \"Timeline.1\", \"name\": \"cut\"," \ " \"metadata\": {\"sync\": {\"guid\": \"cut\"}}," \ " \"tracks\": {\"OTIO_SCHEMA\": \"Stack.1\"," \ " \"metadata\": {\"sync\": {\"guid\": \"tracks\"}}, \"children\": [" \ "{\"OTIO_SCHEMA\": \"Track.1\", \"name\": \"V1\", \"kind\": \"Video\"," \ " \"metadata\": {\"sync\": {\"guid\": \"v1\"}}," \ " \"children\": [" first ", " second "]}]}}}" static const char BEFORE[] = SNAPSHOT("1", CLIP("A", "a"), CLIP("B", "b")); static const char AFTER[] = SNAPSHOT("2", CLIP("B", "b"), CLIP("A", "a")); int main(void) { OtioBuffer error = {0}, patches = {0}, stats = {0}; if (otio_ext_sync_enable(&error) != OTIO_STATUS_OK || otio_ext_sync_diff(BEFORE, AFTER, true, &patches, &error) != OTIO_STATUS_OK || otio_ext_sync_diff_stats(BEFORE, AFTER, &stats, &error) != OTIO_STATUS_OK) { fprintf(stderr, "%s\n", error.data); otio_buffer_free(error); return 1; } /* One move, of A to after B: a patch names objects by GUID. */ printf("%s\n%s\n", patches.data, stats.data); otio_buffer_free(patches); otio_buffer_free(stats); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/diff-two-timelines/c.c **Record a transaction.** Begin a transaction on a timeline, edit it with any of the usual calls, and commit it for the patches. Rolling back puts the timeline back as it was. The transaction remembers the timeline as it began and diffs it against the timeline as it is, so its patches are exactly what comparing the two versions gives. Applying patches to a timeline changes it in place. The timeline keeps its identity, and so does every track and item the patches keep, so anything your program holds onto still refers to the same object afterwards. An object the patches removed is gone, and one they added can be found by its GUID. ```c #include #include "otio_ext_sync.h" /* A cut, as an .otio file would hold it. Nothing in it has a GUID yet. */ static const char CUT[] = "{\"OTIO_SCHEMA\": \"Timeline.1\", \"name\": \"cut\"," " \"tracks\": {\"OTIO_SCHEMA\": \"Stack.1\", \"children\": [" "{\"OTIO_SCHEMA\": \"Track.1\", \"name\": \"V1\", \"kind\": \"Video\", \"children\": [" "{\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"A\"}," " {\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"B\"}]}]}}"; int main(void) { OtioBuffer error = {0}, before = {0}, json = {0}, patches = {0}, name = {0}; OtioDocument *editor = NULL, *viewer = NULL; OtioNode timeline = otio_node_none(), shown = otio_node_none(); int failed = 1; if (otio_ext_sync_enable(&error) != OTIO_STATUS_OK || otio_document_from_json(CUT, &editor, &error) != OTIO_STATUS_OK || otio_document_root(editor, &timeline, &error) != OTIO_STATUS_OK) { goto done; } /* Beginning a transaction gives every object a GUID, and writes the * timeline as it was as a snapshot: that snapshot is the transaction. * A second program starts from the same stamped timeline. */ if (otio_ext_sync_begin(editor, timeline, "main", 1, &before, &error) != OTIO_STATUS_OK || otio_document_to_json(editor, 0, &json, &error) != OTIO_STATUS_OK || otio_document_from_json(json.data, &viewer, &error) != OTIO_STATUS_OK || otio_document_root(viewer, &shown, &error) != OTIO_STATUS_OK) { goto done; } /* Edit as usual, then commit for the patches that say what changed. */ if (otio_node_set_name(editor, timeline, "fine cut", &error) != OTIO_STATUS_OK || otio_ext_sync_commit(editor, timeline, before.data, &patches, &error) != OTIO_STATUS_OK) { goto done; } printf("%s\n", patches.data); /* The other copy applies them in place: what it held still names the * same objects. */ if (otio_ext_sync_apply_to(viewer, shown, patches.data, &error) != OTIO_STATUS_OK || otio_node_name(viewer, shown, &name, &error) != OTIO_STATUS_OK) { goto done; } printf("the viewer now shows %s\n", name.data); failed = 0; done: if (failed) { fprintf(stderr, "%s\n", error.data); } otio_buffer_free(error); otio_buffer_free(before); otio_buffer_free(json); otio_buffer_free(patches); otio_buffer_free(name); otio_document_free(editor); otio_document_free(viewer); return failed; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/apply-patches/c.c #### Noticing changes A program that edits a timeline through code it does not own, such as an editing operation or a script, can turn on the document's change journal. The journal records which objects any edit touched. Committing it runs the `document_changed` hook, with the GUIDs of the touched objects as its `touched` argument, so a hook script can send the change on. The journal is off unless you turn it on, and costs nothing while it is off. The design is in [ADR 0011](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0011-timeline-sync.md). #### Sessions A session joins a shared timeline and keeps a copy of it in step. It does no networking itself: a program hands it what arrived and the session hands back what to send and what happened. The `otio-ext-sync-transport` crate puts it on a wire. Its first wire format is Furnace's protocol v2, and it has a blocking WebSocket client that runs the whole loop: ```rust use std::time::Duration; use otio_ext_sync::session::{self, ApplyOptions, Config, Event, Session}; use otio_ext_sync_transport::websocket::Client; let config = Config { token: join_token, ..Config::default() }; let mut client = Client::connect(&ws_url, Session::new(config))?; while let Some(event) = client.next_event(Duration::from_secs(1))? { // Applies timeline changes to `timeline` in place and runs the hooks. session::handle(&mut document, timeline, &event, &ApplyOptions::default())?; } ``` The host publishes with `session.publish(&mut document, timeline, "main")`, which works out the patches since the last publish and sends them. Viewers receive each change as patches and apply them in place, so handles to the timeline, its tracks and its items keep working. A host that reconnects to a Furnace holding an older generation than its own, because the connection dropped as an edit went out, sends its timeline again. ##### Patches on the wire When Furnace's welcome lists `timeline.patch`, the host sends each change as a `patch` message holding the patches and the generations they go from and to. Furnace applies them to its copy, so anyone who joins later still gets the whole timeline in their welcome, and relays them to every client whose hello listed `timeline.patch.receive` (sessions list it unless told not to). Everyone else, older clients included, gets the timeline Furnace rebuilt, as before. The host still sends the whole timeline for its first publish, every 64th change after that as a checkpoint, when the patches would be bigger than the timeline, when the change replaces the timeline outright, and to a Furnace that does not list `timeline.patch`. If Furnace's copy is not the generation a patch starts from, or the patch does not apply to it, Furnace answers `stale_patch` or `patch_rejected`, and the host sends its whole timeline in reply. A viewer that receives patches from a generation it does not have is out of sync: `receive` fails with an out-of-sync error (a `ConnectionError` in Python, `IoError` over the C interface), and the program reconnects to get the whole timeline in the welcome. Furnace's conformance corpus, revision 5, holds a patch and the timeline it makes, and Go and Rust must both reach the same timeline. Everything a session sends travels on a channel with a delivery rule: | Channel | Rule | Over Furnace | | --- | --- | --- | | timeline | in order | `patch`, or `snapshot`, from the host only | | playback | newest wins: a playhead not yet sent is replaced | `presence` | | presence | newest wins | `welcome` and `roster` | | review | kept until acknowledged, refused or cancelled, sent again after a reconnect | `review_commit` | | signal | sent once, never replayed | `signal` | Only the host may change the timeline, and only a client with the `review.comment.create` capability may commit a review comment. A session refuses these itself rather than letting Furnace close the connection. For the same reason `session.send` carries only Furnace's message types that have no call of their own (`signal`, and from the host only `bin` and the LAN messages), and review comments are refused when an older Furnace chooses protocol v1, which has no review ledger. Selections and other extensions' channels need Furnace to relay them, which it does not do yet. Four hooks run around a session: `sync_before_send` before the host's changes leave (a script that fails stops them), `sync_before_apply` before a remote change is applied (a script that fails rejects it), `sync_after_apply` after, and `sync_joined` once the join is done. The media linker runs on the clips a remote change inserts. ##### From other languages Every SDK has the same session, over the C interface. The program owns the socket: open a WebSocket to Furnace with whatever your language offers, connect the session, and then keep three things going. Send every frame the session gives you (text frames as text, binary ones as binary), hand it every frame that arrives, and act on each event it reports. Passing a `timeline` or `joined` event to `handle` with your copy of the timeline applies it in place and runs the hooks, as `session::handle` does in Rust. A session is a number from `session_new`, released with `session_close`. Its settings, its events and its state are JSON objects, in the field names Furnace uses: | | Fields | | --- | --- | | Settings | `token`, `identity` (`displayName`, `app`, `appVersion`, `credential`, `deviceId`, `clientKind`, `formFactor`, `deviceName`, `capabilities`), `encodings` (`br`, `gzip`); all optional | | Event | `type`: `joined`, `timeline`, `timeline_removed`, `playhead`, `selection`, `roster`, `leader`, `review_relay`, `review_committed`, `review_rejected`, `error`, `ended` or `message`, and that event's fields | | State | `joined`, `ended`, `sessionId`, `selfId`, `role`, `leader`, `peers`, `activeSequence`, `sequences`, `pendingReviews`, `version`, `encodings` | Python wraps this in a `Session` class that takes and returns dictionaries, and `poll_transmit` there returns `str` for a text frame and `bytes` for a binary one. ```c #include #include #include #include "otio_ext_sync.h" /* What Furnace sends a viewer joining a session whose host shared one * timeline. */ static const char WELCOME[] = "{\"v\":2,\"type\":\"welcome\",\"sessionId\":\"s_1\",\"seq\":0,\"ts\":0,\"order\":1," "\"payload\":{\"selfId\":\"p_2\",\"role\":\"viewer\"," "\"roster\":[{\"id\":\"p_1\",\"displayName\":\"Host\",\"role\":\"host\",\"joinedAt\":0," "\"connectionState\":\"connected\",\"muted\":false}]," "\"session\":{\"id\":\"s_1\",\"createdAt\":0}," "\"snapshot\":{\"sequenceId\":\"main\",\"generation\":1," "\"otioJson\":{\"OTIO_SCHEMA\":\"Timeline.1\",\"name\":\"Coastline\"," "\"metadata\":{\"sync\":{\"guid\":\"t-1\"}}," "\"tracks\":{\"OTIO_SCHEMA\":\"Stack.1\",\"metadata\":{\"sync\":{\"guid\":\"s-1\"}}," "\"children\":[{\"OTIO_SCHEMA\":\"Track.1\",\"name\":\"V1\",\"kind\":\"Video\"," "\"metadata\":{\"sync\":{\"guid\":\"v-1\"}}," "\"children\":[{\"OTIO_SCHEMA\":\"Clip.2\",\"name\":\"Waves\"," "\"metadata\":{\"sync\":{\"guid\":\"c-1\"}}}]}]}}}}}"; /* Sends every frame the session has waiting. A real program writes each one * to its WebSocket to Furnace; this one only says what it would send. */ static OtioStatus send_frames(uint64_t session, OtioBuffer *error) { for (;;) { OtioBuffer frame = {0}; bool binary = false; OtioStatus status = otio_ext_sync_session_poll_transmit(session, 0, &frame, &binary, error); if (status == OTIO_STATUS_NO_VALUE) { /* Nothing left to send. The library still wrote a message to * error, which this program owns. */ otio_buffer_free(*error); *error = (OtioBuffer){0}; return OTIO_STATUS_OK; } if (status != OTIO_STATUS_OK) { return status; } printf("send %zu bytes as %s\n", frame.len, binary ? "binary" : "text"); otio_buffer_free(frame); } } /* Prints an event's type. A real program reads the event with its JSON * library. */ static void print_type(const char *event) { const char *type = strstr(event, "\"type\":\""); if (type != NULL) { type += strlen("\"type\":\""); printf("event %.*s: ", (int)strcspn(type, "\""), type); } } int main(void) { OtioBuffer error = {0}, name = {0}; OtioDocument *copy = NULL; OtioNode timeline = otio_node_none(); uint64_t session = 0; int failed = 1; /* A session owns no socket. Connect once yours is open: the hello is * the first frame to send. */ if (otio_ext_sync_enable(&error) != OTIO_STATUS_OK || otio_ext_sync_session_new("{\"token\": \"join-code\", \"identity\": {\"displayName\": \"Ada\"}}", &session, &error) != OTIO_STATUS_OK || otio_ext_sync_session_connect(session, &error) != OTIO_STATUS_OK || send_frames(session, &error) != OTIO_STATUS_OK) { goto done; } /* The frame Furnace sends back. */ if (otio_ext_sync_session_receive(session, (const uint8_t *)WELCOME, strlen(WELCOME), false, &error) != OTIO_STATUS_OK) { goto done; } /* The program's own copy of the timeline starts empty. Each event goes * to otio_ext_sync_handle, which applies what it can and says what it * did. */ if (otio_document_from_json("{\"OTIO_SCHEMA\": \"Timeline.1\"}", ©, &error) != OTIO_STATUS_OK || otio_document_root(copy, &timeline, &error) != OTIO_STATUS_OK) { goto done; } for (;;) { OtioBuffer event = {0}, handled = {0}; OtioStatus status = otio_ext_sync_session_poll_event(session, &event, &error); if (status == OTIO_STATUS_NO_VALUE) { otio_buffer_free(error); error = (OtioBuffer){0}; break; } if (status == OTIO_STATUS_OK) { print_type(event.data); status = otio_ext_sync_handle(copy, timeline, event.data, &handled, &error); } if (status == OTIO_STATUS_OK) { printf("%s\n", handled.data); } otio_buffer_free(event); otio_buffer_free(handled); if (status != OTIO_STATUS_OK) { goto done; } } if (otio_node_name(copy, timeline, &name, &error) != OTIO_STATUS_OK) { goto done; } printf("joined, showing %s\n", name.data); /* Move the playhead, and send what that says to everyone else. */ if (otio_ext_sync_session_set_playhead(session, "main", 24, true, 1, &error) != OTIO_STATUS_OK || send_frames(session, &error) != OTIO_STATUS_OK) { goto done; } /* Leaving queues a goodbye to send before the socket closes. Closing * the session at the end releases it. */ if (otio_ext_sync_session_leave(session, &error) != OTIO_STATUS_OK || send_frames(session, &error) != OTIO_STATUS_OK) { goto done; } failed = 0; done: if (failed) { fprintf(stderr, "%s\n", error.data); } otio_ext_sync_session_close(session, NULL); otio_buffer_free(error); otio_buffer_free(name); otio_document_free(copy); return failed; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/join-a-session/c.c ##### Compression Each side lists the encodings it accepts when it joins: the client in its hello, the server in its welcome. Messages of 1 KiB or more go as Brotli or gzip when the other side accepts it, in a binary frame whose first byte names the encoding (1 for gzip, 2 for Brotli); everything else stays JSON text. Furnace does not list any encodings yet, so today every frame is text; the in-memory test room does, and CI publishes a table of what each encoding saves on the repository's timelines. A 103 KiB timeline goes over the wire as about 3 KiB. ##### Checked against Furnace Furnace's conformance corpus, every frame it says a client or server sends, is vendored into the crate's tests: each client frame is read and written again and must match it value for value, and each server frame is read as what it says. A CI job also builds furnaced and has a host and a viewer join a real session. #### Sessions over ORI Sync The ORI profile speaks ORI Sync's wire, so a session can join the same RabbitMQ session as OpenRV and xStudio running ORI's plugins. It is in Rust for now. There is no server: every peer binds a queue to the session's fanout exchange, `sync_session_`, and hears everything. The first peer finds no master within two seconds and elects itself; later peers ask the master for its state and join with it, and if the master stops answering another peer takes over. ```rust use std::time::Duration; use otio_ext_sync::session::{self, ApplyOptions, Config, Session}; use otio_ext_sync_transport::{amqp::Client, ori}; let options = ori::Options::new("review-42"); let mut client = Client::connect("amqp://guest:guest@localhost:5672/", Session::new(Config::default()), options)?; loop { // `None` is a quiet second, not the end: keep going so the client can // elect itself, answer joiners and send its heartbeat. if let Some(event) = client.next_event(Duration::from_secs(1))? { session::handle(&mut document, timeline, &event, &ApplyOptions::default())?; } } ``` Any peer may change the timelines, as in ORI. What ORI's peers send arrives as patches like Furnace's, and what a session publishes goes out as ORI's own edits (`SET_PROPERTY`, `INSERT_CHILD`, `MOVE_CHILD`, `REMOVE_CHILD`), or as the whole timeline when ORI's edits cannot say it, such as inserting a track with clips in it. A changed annotation clip goes out as ORI's `REPLACE_ANNOTATION_COMMANDS`; see [ORI Annotations](https://otio-rust-docs.vercel.app/docs/extensions/ori-annotations/#on-ori-s-wire). Several timelines, which one is active, the playhead and ORI's display settings cross too. Selections are retired in ORI, and review comments, leaders and muting have no ORI message, so sending them fails as unsupported. Snapshots and messages over 1 MB can be zlib-compressed, but ORI's own peers cannot read either yet, so both are off unless `Options` turns them on. Compressed input is always read. ##### Checked against ORI ORI's own recordings of real sessions are vendored into the crate's tests and replayed as ORI's recording player replays them; the session must end with the same timelines, active timeline, playhead and display settings as ORI's own peer, which CI checks against ORI itself. CI also runs sessions on a real RabbitMQ, between Rust peers and with ORI's Python peer, with each side as master. --- Page: https://otio-rust-docs.vercel.app/docs/extensions/timeline-sync/ ### ORI Annotations > Reading, writing and editing the ORI project's review annotations on an OpenTimelineIO timeline. ORI Annotations are drawings and notes made on a frame during review: paint strokes, text and shapes. The schemas belong to the ASWF's ORI project, whose [ORI Annotations](https://github.com/richardssam/ORIAnnotations) repository defines them as an OpenTimelineIO plugin in Python and exchanges them live between OpenRV and xStudio. This extension reads and writes the same objects, under the same names, so a file written here opens in ORI's tools and the other way round. It is built on [timeline sync](https://otio-rust-docs.vercel.app/docs/extensions/timeline-sync/): enabling it enables sync, and every object it creates carries a sync GUID. #### Where it stands The extension is being built in phases, tracked in [epic #126](https://github.com/alchemist-editor/otio-rust/issues/126): 1. **Schemas, the annotation track and coordinates**, in Rust. 2. **Every language**: the C interface, a module in every SDK, and Python. 3. **Live review over Furnace**: review comments with their strokes, folded into the host's timeline, and strokes in progress. 4. **ORI's wire**, for live review with RV and xStudio. All four are here. One piece waits on the server: Furnace does not yet pass strokes in progress along, so over Furnace only finished comments travel today; over ORI's wire both do. See [Live review](#live-review). #### Turning it on | Language | Import | Enable | | --- | --- | --- | | Rust | `otio-ext-ori-annotations` | `otio_ext_ori_annotations::install(); otio_ext::enable(otio_ext_ori_annotations::ID)?;` | | Python | `from opentimelineio.ext import ori` | `ori.enable()` | | C | `#include "otio_ext_ori.h"` | `otio_ext_ori_enable(&error);` | | Go | `github.com/alchemist-editor/otio-rust/sdk/go/ext/ori` | `ori.Enable()` | | Swift | `import OTIOExtOri` | `try Ori.enable()` | | TypeScript | `import * as ori from "@alchemist-edit/otio/ext/ori"` | `await init(); ori.enable();` | | C++ | `opentimelineio/ext/ori.hpp` | `otio::ext::ori::enable();` | | C# | `using OpenTimelineIO.Ext.Ori;` | `OriExtension.Enable();` | | Objective-C | `#import ` | `OTIOExtOriEnable(&error);` | | Zig | `otio.ext.ori` | `try otio.ext.ori.enable();` | The id is `ori-annotations`. While it is off, `PaintStart.1` and the rest read as unknown schemas and are written back unchanged, exactly as upstream OpenTimelineIO does when ORI's plugin is not on its path; the annotation calls refuse to run. With it on, ORI's files still come out byte for byte as they went in. #### Commands An annotation is a list of commands. A stroke is three or more commands that share one `uuid`: a `PaintStart.1` with the colour, brush and who drew it, one or more `PaintPoint.1` batches of points, and a `PaintEnd.1`. Text and shapes are one command each. | Schema | Holds | | --- | --- | | `PaintStart.1` | Opens a stroke: `uuid`, `rgba`, `brush`, `type`, `participant_hash`, `friendly_name`, and ORI's ghosting and hold fields | | `PaintPoint.1` | A batch of points: a `PaintVertices.1` with parallel `x`, `y`, `size` and `alpha` arrays | | `PaintEnd.1` | Closes a stroke | | `TextAnnotation.1` | Text at a `position`, with `font`, `font_size`, `scale`, `rotation` and `spacing` | | `EllipseAnnotation.1`, `RectangleAnnotation.1` | A shape in the box from `min` to `max`, with outline and fill colours | | `ArrowAnnotation.1` | An arrow from `start` to `end` | The batch of points is written `PaintPoint.1`, singular, as ORI's code and files write it; ORI's prose sometimes says `PaintPoints.1`. ORI's playback events (`Play.1`, `SetCurrentFrame.1`, `SyncPlayback.1` and the others) are registered too, so ORI's recordings read as typed objects. In Rust, `Command::read` gives a typed copy of a command and keeps every field it does not name; a new command built with a constructor such as `PaintStart::new` has the same defaults ORI's Python class gives it. Every other language passes commands as JSON text: a list of ORI objects, exactly as an annotation clip's `annotation_commands` holds them in a file. A field a command leaves out takes the default ORI's class gives it, as reading it with ORI's plugin would, so `{"OTIO_SCHEMA": "PaintStart.1"}` has ORI's `"brush": "circle"`. Python's calls also take a list of dicts. #### Where annotations live A frame's annotations sit on a one-frame clip in a track named `Annotations`, beside the media: ```text Stack ├─ Track "V1" Clip "shot" metadata.sync.guid = "shot" └─ Track "Annotations" Gap Clip "Annotation_10" source_range: frame 10, one frame long metadata.clip_guid = "shot" metadata.annotation_commands = [PaintStart, PaintPoint, PaintEnd, …] ``` The annotation clip's start time is the annotated frame of the media clip, and `clip_guid` names that clip by its sync GUID. This is the form ORI's sync protocol and Alchemist's review design both chose. It follows ORI exactly, including one surprise: an annotation clip for a frame later than the end of the track is placed after a gap at its own frame, but one for an earlier frame is simply added at the end, so annotations are found by `clip_guid` and frame, never by where they sit on the track. Reading also understands two older forms ORI has written: its review export, where the track is named after the review and each clip names the media in `annotated_clip_name`, and `AnnotationEffect.1`, an effect on the media clip that holds its commands. Neither is written. ```c #include #include "otio_ext_ori.h" /* A cut with one shot, which sync knows by the GUID `shot`. */ static const char CUT[] = "{\"OTIO_SCHEMA\": \"Timeline.1\", \"name\": \"review\"," " \"tracks\": {\"OTIO_SCHEMA\": \"Stack.1\", \"children\": [" " {\"OTIO_SCHEMA\": \"Track.1\", \"name\": \"V1\", \"kind\": \"Video\", \"children\": [" " {\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"shot\"," " \"metadata\": {\"sync\": {\"guid\": \"shot\"}}}]}]}}"; /* Stops at the first failure, saying why. */ #define CHECK(call) \ do { \ if ((call) != OTIO_STATUS_OK) { \ fprintf(stderr, "%s\n", error.data ? error.data : #call); \ return 1; \ } \ } while (0) int main(void) { OtioBuffer error = {0}; /* ORI Annotations is an extension; enabling it enables sync too. */ CHECK(otio_ext_ori_enable(&error)); OtioDocument *document = NULL; OtioNode timeline; CHECK(otio_document_from_json(CUT, &document, &error)); CHECK(otio_document_root(document, &timeline, &error)); /* The Annotations track, and the clip on it for frame 10 of the shot. */ OtioNode track, frame; CHECK(otio_ext_ori_ensure_track(document, timeline, &track, &error)); CHECK(otio_ext_ori_ensure_clip(document, track, "shot", 10, 24, &frame, &error)); /* ORI's coordinates are height-normalised with the origin at the * centre, so convert from the pixels of the 1920x1080 picture. */ const double pixels[3][2] = {{200, 200}, {960, 540}, {1720, 880}}; double x[3], y[3]; for (int i = 0; i < 3; i++) { CHECK(otio_ext_ori_pixels_to_ori(1920, 1080, pixels[i][0], pixels[i][1], &x[i], &y[i], &error)); } /* A stroke is a PaintStart, its points and a PaintEnd sharing one uuid. * Fields left out take the defaults ORI's classes give them. */ OtioBuffer uuid = {0}; CHECK(otio_ext_ori_new_uuid(&uuid, &error)); char stroke[1024]; snprintf(stroke, sizeof stroke, "[{\"OTIO_SCHEMA\":\"PaintStart.1\",\"uuid\":\"%s\",\"rgba\":[1,0.25,0.25,1]," "\"participant_hash\":\"reviewer-1\"}," "{\"OTIO_SCHEMA\":\"PaintPoint.1\",\"uuid\":\"%s\",\"points\":{" "\"OTIO_SCHEMA\":\"PaintVertices.1\",\"x\":[%.17g,%.17g,%.17g]," "\"y\":[%.17g,%.17g,%.17g],\"size\":[0.01,0.01,0.01],\"alpha\":[1,1,1]}}," "{\"OTIO_SCHEMA\":\"PaintEnd.1\",\"uuid\":\"%s\"}]", uuid.data, uuid.data, x[0], x[1], x[2], y[0], y[1], y[2], uuid.data); otio_buffer_free(uuid); CHECK(otio_ext_ori_add_commands(document, frame, stroke, &error)); /* Read them back: every annotation on the shot. */ OtioNode holders[8]; size_t count = 0; CHECK(otio_ext_ori_list_for_clip(document, timeline, "shot", holders, 8, &count, &error)); for (size_t i = 0; i < count && i < 8; i++) { OtioBuffer about = {0}; CHECK(otio_ext_ori_describe(document, holders[i], &about, &error)); printf("%s\n", about.data); otio_buffer_free(about); } /* The file is plain OpenTimelineIO with ORI's schemas in it. */ OtioBuffer json = {0}; CHECK(otio_document_to_json(document, 4, &json, &error)); printf("%s\n", json.data); otio_buffer_free(json); otio_document_free(document); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/annotate-a-frame/c.c The calls, named as in Rust's `annotations` module and in every language's own case: | Call | Does | | --- | --- | | `find_track`, `ensure_track` | Finds the timeline's annotation track, or adds one | | `find_clip`, `ensure_clip` | Finds the annotation clip for a media clip GUID and frame, or adds one | | `add_commands` | Adds commands at the end of an annotation clip | | `replace_commands` | Replaces every command with a uuid, such as one whole stroke, in place | | `remove_commands` | Removes every command with any of a list of uuids | | `set_commands` | Replaces all of a clip's commands, as ORI's replace does; an empty list clears it | | `list`, `list_for_clip`, `list_at_frame` | The object holding every annotation on the timeline, or those for a clip or a frame (`for_clip` and `at_frame` in Rust, which also has `by_participant`) | | `describe`, `commands` | What an annotation says about itself, as JSON, and its commands | Edits go by uuid, so one person's edit never disturbs another person's strokes. #### Live review Annotations travel between the people in a [sync session](https://otio-rust-docs.vercel.app/docs/extensions/timeline-sync/) in two ways. **A finished comment** is a note with the strokes drawn for it. A reviewer commits it with `commit`, giving the session, their copy of the timeline, the note, the commands, and an anchor saying where they drew: ```json {"sequenceId": "main", "capturedGeneration": 1, "clipGuid": "main/shot", "sourceTime": {"value": 10, "rate": 24}, "capturedSequenceTime": {"value": 10, "rate": 24}, "capturedAt": 1787742000000} ``` The comment goes on the session's review ledger, so it is sent again after every reconnect until Furnace stores it. Furnace then relays it to everyone as a `review_relay` event, and the host folds it into its timeline with `fold`: the strokes merge into the annotation clip for that source frame, and the comment is recorded on the clip: ```text Clip "Annotation_10" metadata.clip_guid = "main/shot" metadata.annotation_commands = [PaintStart s1, PaintPoint s1, PaintEnd s1] metadata.note = "Too dark here." the comments' notes, joined metadata.status = "open" metadata.alchemist.comments = [{commentId, authorUserId, bodyMarkdown, commandUuids: ["s1"], state, revision}] ``` Folding the same comment twice changes nothing, and `fold` says so. A stroke belongs to the user whose comment added it (`owner_of`): `commit` and `fold` both refuse a comment that would replace someone else's stroke, or a stroke no comment owns. The host then publishes its timeline as usual, so everyone sees the annotation. **A stroke in progress** is what ORI calls a `PARTIAL`: the commands drawn so far, shown to the others while the pen is still down and then dropped. It never reaches a timeline. `send_partial` sends one, and `read_partial` reads one out of a session event: ```json {"clip_guid": "main/shot", "frame": 10, "fps": 24, "events": [{"OTIO_SCHEMA": "PaintStart.1", "uuid": "s2"}]} ``` Over Furnace a stroke in progress has nowhere to go yet: Furnace closes the connection on a message type it does not know, so the session refuses to send one (an unsupported failure) rather than lose the connection. Over ORI's wire it travels as `Annotation.1/PARTIAL`, as RV and xStudio send it. ```c #include #include #include #include "otio_ext_ori.h" #include "otio_ext_sync.h" /* The cut both sides have: one shot, which sync knows by the GUID * main/shot. */ static const char CUT[] = "{\"OTIO_SCHEMA\":\"Timeline.1\",\"name\":\"review\",\"metadata\":{\"sync\":{\"guid\"" ":\"main\"}},\"tracks\":{\"OTIO_SCHEMA\":\"Stack.1\",\"children\":[{\"OTIO_SCHEMA\"" ":\"Track.1\",\"name\":\"V1\",\"kind\":\"Video\",\"children\":[{\"OTIO_SCHEMA\":\"Clip" ".2\",\"name\":\"shot\",\"metadata\":{\"sync\":{\"guid\":\"main/shot\"}},\"source_ran" "ge\":{\"OTIO_SCHEMA\":\"TimeRange.1\",\"start_time\":{\"OTIO_SCHEMA\":\"Rational" "Time.1\",\"value\":0,\"rate\":24},\"duration\":{\"OTIO_SCHEMA\":\"RationalTime.1" "\",\"value\":48,\"rate\":24}}}]}]}}"; /* What Furnace tells a reviewer who joins: who they are, and that they may * comment. */ static const char WELCOME[] = "{\"v\":2,\"type\":\"welcome\",\"sessionId\":\"s_1\",\"seq\":0,\"ts\":0,\"order\":1,\"pa" "yload\":{\"selfId\":\"p_2\",\"selfUserId\":\"u_bea\",\"role\":\"viewer\",\"roster\":[" "],\"session\":{\"id\":\"s_1\",\"createdAt\":0}}}"; /* What Furnace tells the host when it opens the session. */ static const char HOST_WELCOME[] = "{\"v\":2,\"type\":\"welcome\",\"sessionId\":\"s_1\",\"seq\":0,\"ts\":0,\"order\":1,\"pa" "yload\":{\"selfId\":\"p_1\",\"selfUserId\":\"u_ari\",\"role\":\"host\",\"roster\":[]," "\"session\":{\"id\":\"s_1\",\"createdAt\":0}}}"; /* Where the reviewer drew: frame 10 of the shot, as the program shows it. */ static const char ANCHOR[] = "{\"sequenceId\":\"main\",\"capturedGeneration\":1,\"clipGuid\":\"main/shot\",\"so" "urceTime\":{\"value\":10,\"rate\":24},\"capturedSequenceTime\":{\"value\":10,\"r" "ate\":24},\"capturedAt\":1787742000000}"; /* One stroke: a PaintStart, its points and a PaintEnd sharing a uuid. */ static const char STROKE[] = "[{\"OTIO_SCHEMA\":\"PaintStart.1\",\"uuid\":\"s1\",\"rgba\":[1,0.25,0.25,1]},{\"O" "TIO_SCHEMA\":\"PaintPoint.1\",\"uuid\":\"s1\",\"points\":{\"OTIO_SCHEMA\":\"PaintV" "ertices.1\",\"x\":[-0.5,0,0.5],\"y\":[0.2,0,-0.2],\"size\":[0.01,0.01,0.01],\"" "alpha\":[1,1,1]}},{\"OTIO_SCHEMA\":\"PaintEnd.1\",\"uuid\":\"s1\"}]"; /* The comment as Furnace relays it once it has stored it, to everyone in * the session. A real host reads this from its own socket. */ static const char RELAY[] = "{\"v\":2,\"type\":\"review_commit\",\"sessionId\":\"s_1\",\"senderId\":\"p_2\",\"seq\"" ":1,\"ts\":0,\"order\":2,\"payload\":{\"clientMutationId\":\"m1\",\"commentId\":\"c1" "\",\"authorUserId\":\"u_bea\",\"authorPresenceId\":\"p_2\",\"serverOrder\":1,\"rev" "ision\":1,\"createdAt\":0,\"anchor\":{\"sequenceId\":\"main\",\"capturedGenerati" "on\":1,\"clipGuid\":\"main/shot\",\"sourceTime\":{\"value\":10,\"rate\":24},\"capt" "uredSequenceTime\":{\"value\":10,\"rate\":24},\"capturedAt\":1787742000000},\"" "bodyMarkdown\":\"Too dark here.\",\"annotationCommands\":[{\"OTIO_SCHEMA\":\"P" "aintStart.1\",\"uuid\":\"s1\",\"rgba\":[1,0.25,0.25,1]},{\"OTIO_SCHEMA\":\"Paint" "Point.1\",\"uuid\":\"s1\",\"points\":{\"OTIO_SCHEMA\":\"PaintVertices.1\",\"x\":[-0" ".5,0,0.5],\"y\":[0.2,0,-0.2],\"size\":[0.01,0.01,0.01],\"alpha\":[1,1,1]}},{" "\"OTIO_SCHEMA\":\"PaintEnd.1\",\"uuid\":\"s1\"}]}}"; /* Stops at the first failure, saying why. */ #define CHECK(call) \ do { \ if ((call) != OTIO_STATUS_OK) { \ fprintf(stderr, "%s\n", error.data ? error.data : #call); \ return 1; \ } \ } while (0) /* OTIO_STATUS_NO_VALUE is an answer, not a failure: there is nothing more. * The library still wrote a message, which the program owns. */ static int nothing_more(OtioStatus status, OtioBuffer *error) { if (status != OTIO_STATUS_NO_VALUE) { return 0; } otio_buffer_free(*error); *error = (OtioBuffer){0}; return 1; } /* Stands in for the network: a real program writes every frame the session * has waiting to its WebSocket to Furnace. */ static OtioStatus send_frames(uint64_t session, OtioBuffer *error) { for (;;) { OtioBuffer frame = {0}; bool binary = false; OtioStatus status = otio_ext_sync_session_poll_transmit(session, 0, &frame, &binary, error); if (nothing_more(status, error)) { return OTIO_STATUS_OK; } if (status != OTIO_STATUS_OK) { return status; } printf("send %zu bytes\n", frame.len); otio_buffer_free(frame); } } static OtioStatus receive(uint64_t session, const char *frame, OtioBuffer *error) { return otio_ext_sync_session_receive(session, (const uint8_t *)frame, strlen(frame), false, error); } int main(void) { OtioBuffer error = {0}; /* ORI Annotations is an extension; enabling it enables sync too. */ CHECK(otio_ext_ori_enable(&error)); /* The reviewer joins with the right to comment. */ uint64_t reviewer = 0; CHECK(otio_ext_sync_session_new("{\"token\":\"join-code\",\"identity\":{\"capabilities\":[\"review.comment.create\",\"review.annotation.create\"]}}", &reviewer, &error)); CHECK(otio_ext_sync_session_connect(reviewer, &error)); CHECK(send_frames(reviewer, &error)); CHECK(receive(reviewer, WELCOME, &error)); for (;;) { OtioBuffer event = {0}; OtioStatus status = otio_ext_sync_session_poll_event(reviewer, &event, &error); if (nothing_more(status, &error)) { break; } CHECK(status); otio_buffer_free(event); } /* Commit the stroke with a note. The session sends it again after every * reconnect until Furnace stores it, so it is not lost. */ OtioDocument *seen = NULL; OtioNode seen_root; OtioBuffer id = {0}; CHECK(otio_document_from_json(CUT, &seen, &error)); CHECK(otio_document_root(seen, &seen_root, &error)); CHECK(otio_ext_ori_commit(reviewer, seen, seen_root, ANCHOR, "Too dark here.", STROKE, "", &id, &error)); printf("committed %s\n", id.data); otio_buffer_free(id); otio_document_free(seen); CHECK(send_frames(reviewer, &error)); /* The host hears the stored comment and folds it into its timeline. */ uint64_t host = 0; CHECK(otio_ext_sync_session_new("{\"token\":\"host-token\",\"identity\":{\"capabilities\":[\"review.commit.receive\"]}}", &host, &error)); CHECK(otio_ext_sync_session_connect(host, &error)); CHECK(send_frames(host, &error)); CHECK(receive(host, HOST_WELCOME, &error)); CHECK(receive(host, RELAY, &error)); OtioDocument *document = NULL; OtioNode timeline; CHECK(otio_document_from_json(CUT, &document, &error)); CHECK(otio_document_root(document, &timeline, &error)); for (;;) { OtioBuffer event = {0}, partial = {0}; OtioStatus status = otio_ext_sync_session_poll_event(host, &event, &error); if (nothing_more(status, &error)) { break; } CHECK(status); /* A stroke still being drawn: draw it over the picture, then drop it. */ status = otio_ext_ori_read_partial(event.data, &partial, &error); if (status == OTIO_STATUS_OK) { printf("drawing %s\n", partial.data); otio_buffer_free(partial); } else if (!nothing_more(status, &error)) { CHECK(status); } else if (strstr(event.data, "\"type\":\"review_relay\"") != NULL) { OtioNode clip; bool already = false; OtioBuffer about = {0}, owner = {0}; CHECK(otio_ext_ori_fold(document, timeline, event.data, &clip, &already, &error)); CHECK(otio_ext_ori_describe(document, clip, &about, &error)); CHECK(otio_ext_ori_owner_of(document, clip, "s1", &owner, &error)); printf("folded %s already there: %s\n", about.data, already ? "yes" : "no"); printf("the stroke belongs to %s\n", owner.data); otio_buffer_free(about); otio_buffer_free(owner); } otio_buffer_free(event); } otio_document_free(document); CHECK(otio_ext_sync_session_close(reviewer, &error)); CHECK(otio_ext_sync_session_close(host, &error)); return 0; } ``` Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/share-an-annotation/c.c ##### Merging Several people annotating one frame is the normal case, so edits merge rather than overwrite. These are the rules Alchemist's review uses, and the calls are there for any program that keeps annotations itself: | Call | Does | | --- | --- | | `merge_commands` | Merges commands into a clip's by uuid: a uuid it has is replaced where it stands, a new one goes at the end | | `replace_by_kind` | Replaces a clip's strokes, or its text, with new ones, keeping the kinds the new list lacks; an empty list clears all of them. This is how ORI's `REPLACE_ANNOTATION_COMMANDS` reads | | `merge_duplicates` | Folds every second annotation clip for one clip and frame into the first | | `commit` | Commits a comment with its strokes on a session | | `fold` | Folds a relayed comment into the timeline, saying whether it was there already | | `owner_of` | The user whose comment added a stroke | | `send_partial`, `read_partial` | Sends a stroke in progress, and reads one from a session event | ##### On ORI's wire With the [ORI profile](https://otio-rust-docs.vercel.app/docs/extensions/timeline-sync/#sessions-over-ori-sync), a session talks to RV and xStudio directly. An annotation edit from them (`REPLACE_ANNOTATION_COMMANDS`, or an annotation clip in `INSERT_CHILD`) arrives as a timeline change, merged by the rules above. Publishing a timeline whose annotation clip changed sends `REPLACE_ANNOTATION_COMMANDS` for that clip. Strokes in progress go both ways. ORI's roles still apply. A peer is a driver unless its options say otherwise, and only a driver may clear a clip's annotations. A peer joined as a reviewer can send strokes in progress, but cannot yet publish finished ones, because sync lets only the host or a driver publish timeline changes. #### Coordinates ORI places every point in height-normalised coordinates with Y up and the origin at the centre of the picture: `y` runs from -0.5 at the bottom to 0.5 at the top, and `x` from minus to plus half the aspect ratio, ±8/9 on a 16:9 frame. ```text x = (px - width / 2) / height y = -(py - height / 2) / height ``` `coords::ImageSize` converts to and from pixels, a port of ORI's `coords.py` that gives ORI's own test chart its numbers bit for bit; every language has it as `pixels_to_ori` and `ori_to_pixels`. Where a clip's media has `available_image_bounds`, `ori_to_bounds` and `bounds_to_ori` map ORI's space onto OpenTimelineIO's: the bounds' centre is ORI's origin and their height ORI's unit. Without bounds ORI assumes 16:9. Drawing the strokes is a renderer's job, not this extension's: it stores and moves the commands. #### Checked against ORI - ORI's own `annotationreview.otio` and test chart round-trip byte for byte, with the extension on and off. - A review built through these calls is byte for byte the file ORI's `SyncManager` and annotation builder write for the same strokes, generated with ORI's plugin on upstream OpenTimelineIO 0.18.1. - Every command in ORI's test chart reads as a typed command and writes back the same. - ORI's own test suite, run on otio-rust's Python bindings with ORI's plugin, gives exactly what it gives on upstream OpenTimelineIO 0.18.1. - ORI's plugin and this extension work in the same Python program: ORI's classes still read ORI's commands, this extension finds them, and the file writes back byte for byte. - The same review is built and edited by each SDK's own tests. - All 450 strokes in progress in ORI's 26 recorded sessions read as typed commands, and every recorded session's end state reads back the same. - A comment committed by a reviewer over a loopback Furnace room, and the server's own conformance fixture, fold into the host's timeline, in Rust, through the C interface, and in every SDK. See [ADR 0012](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0012-ori-annotations.md) for the design. --- Page: https://otio-rust-docs.vercel.app/docs/extensions/ori-annotations/ ## Internals ### How the SDKs are made > One description of the C ABI, read out of its own source, becomes every binding. Nobody writes these SDKs by hand, and nobody writes an interface description by hand either. ```text crates/otio-capi/src the C ABI, in Rust │ │ otio-sdk-model reads it ▼ sdk/api.json every call, its parameters, its docs, its role │ │ otio-sdk-gen writes one SDK per language ▼ sdk/go sdk/swift sdk/zig sdk/cpp sdk/csharp sdk/objc … and this site's reference pages ``` The description is committed as [`sdk/api.json`](https://github.com/alchemist-editor/otio-rust/blob/main/sdk/api.json), so a change to the C ABI shows up as a readable diff rather than as a surprise inside generated code. `cargo test -p otio-sdk-gen` fails when what is checked in is not what the generator would write today, which is what CI runs. Seven kinds of drift fail that check rather than reaching a user: a C function that fits no naming convention, one whose body does something the description cannot express, a schema the generator does not know about, two functions that would collide in a target language, a call that consumes a document, a struct whose computed layout disagrees with the size the ABI asserts, and generated files that differ from the ones committed. #### What "idiomatic" means here A generated API that reads like a transliteration of C is a failure even if every function is present. Each backend owns the shape of its own language: Go gets methods, embedded structs for the schema ladder, slices, strings, an `error` and a sentinel for "there is no value" — not two-pass out-parameters and status codes. And the shape of the API itself — what things are called, which members exist, which are properties and which are methods — follows upstream OpenTimelineIO's own bindings for that language rather than being invented here. Where a generated SDK departs from upstream's shape, the departure is written down in [ADR 0003](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0003-sdk-generation.md) with its reason. Some of the C ABI is plumbing no SDK user should see. The options structs open with a `struct_size` field so that the library can tell which header a caller was built against; every SDK fills it in as it hands the options over and leaves it out of its own `ReadOptions` and `WriteOptions`, except Zig, whose options are the C struct itself and so keep the field with the package filling it in. The defaults come from `otio_read_options_init` and `otio_write_options_init`, which fill a struct rather than return one, so an SDK sets the size on the struct it passes before the call. The generator does that for any out-parameter of a struct that carries its size, and the SDKs' own `ReadOptionsDefault` calls keep their names. Zig is the interesting exception, because it has no upstream binding to copy. It is also the one target that keeps the document visible, and the reasoning is worth reading if you want to know what the other bindings are hiding. #### Every SDK is held to the same behaviour Each SDK tests its own surface in its own language, which catches a binding that is broken but not one that works and quietly disagrees with the others. So a small set of scenarios is written once, as data beside the description ([`sdk/conformance.json`](https://github.com/alchemist-editor/otio-rust/blob/main/sdk/conformance.json)), and rendered by the generator into every SDK's own test framework: build this timeline and it writes exactly this JSON; a removed object's handle is stale; an object from another timeline is refused before the library is asked, and the other timeline is left whole; an object that already has a parent, or whose handle has gone stale, is refused before its timeline is brought over, so the two never merge. Each language's CI job runs them with the rest of its tests. A failure in them is named by kind — the library's status, or the binding's own refusal — never by message text, so a scenario asks the same question in Go's `error`, Swift's `throws` and Zig's error unions. Zig keeps the document visible, so the scenarios about what hiding it obliges a binding to do are marked as not applying there, and the generator checks that the ones every language runs mean the same thing on both sides of that fork. That rule means every SDK lets you recognise the refusal of another timeline's object without reading its message: | SDK | How to recognise it | | --- | --- | | Go | `errors.Is(err, otio.ErrOtherTimeline)` | | Swift | `error.isOtherTimeline` on `OTIOError` | | Zig | `error.ForeignObject` | | TypeScript | `err instanceof OtherTimelineError` | | C++ | `catch (const otio::OtherTimelineError&)` | | C# | `catch (OtherTimelineException)` | | Objective-C | `OTIOIsOtherTimeline(error)` | In Swift, C++, C# and Objective-C the refusal still carries the invalid-argument status it always had, so code that checked for that keeps working. #### This site is generated from the same file The [reference section](https://otio-rust-docs.vercel.app/reference/) reads `sdk/api.json` at build time: the groups, the calls, their parameters and their documentation all come from the description, and each C declaration is lifted verbatim out of the committed header rather than reassembled from parts. A page here cannot describe a call the library does not have. The prose and the code samples are written by hand, because they are explanation rather than interface. What keeps the samples honest is that each one is compiled — or, for Python, run — by its own language's toolchain against the real SDK, in the same CI job that tests that SDK. A change that leaves a sample describing an API that no longer exists fails the build that made it. A build-time check also requires every sample to exist in every language the switcher offers, or to say in writing why it cannot. --- Page: https://otio-rust-docs.vercel.app/docs/internals/how-the-sdks-are-made/ ## C ABI reference ### Adapter > Reading and writing the interchange formats. Symbols in this group are spelled `otio_read_…` or `otio_write_…`. An SDK drops that prefix. #### `read_from_bytes` `otio_read_from_bytes` · constructor Reads a document from the bytes of a file in some format. `options` may be null for the format's usual behaviour. The bundle formats are refused with `OTIO_STATUS_UNSUPPORTED`: read one from its path. ```c OtioStatus otio_read_from_bytes( OtioFormat format, const uint8_t *data, size_t len, const OtioReadOptions *options, OtioDocument **out_document, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `format` | `OtioFormat` | input | | `data` | `bytes` | bytes | | `len` | `size` | length | | `options` (optional) | `OtioReadOptions` | input | | `out_document` | `document` | output | | `out_error` | `text` | error | #### `read_from_file` `otio_read_from_file` · constructor Reads a document from a file on disk in some format. An AAF is read where it lies, seeking around the file, rather than copied into memory first. An `.otiod` is a directory, and `path` names it. A build without a file system, such as one for WebAssembly, reports `OTIO_STATUS_UNSUPPORTED`; use [`otio_read_from_bytes`] there. ```c * if (otio_read_from_file(OTIO_FORMAT_CMX_3600, "cut.edl", NULL, &document, * &error)) { * fprintf(stderr, "%s\n", error.data); ``` | Parameter | Type | Role | | --- | --- | --- | | `format` | `OtioFormat` | input | | `path` | `text` | input | | `options` (optional) | `OtioReadOptions` | input | | `out_document` | `document` | output | | `out_error` | `text` | error | #### `read_options_default` `otio_read_options_init` · free Returns the defaults, for a caller that wants to change one field. ```c * then call `otio_read_options_init()` or `otio_write_options_init()`, which * fill every field that size covers with its default: * * OtioReadOptions options; ``` | Parameter | Type | Role | | --- | --- | --- | | `out_options` | `OtioReadOptions` | output | #### `write_options_default` `otio_write_options_init` · free Returns the defaults, for a caller that wants to change one field. ```c * then call `otio_read_options_init()` or `otio_write_options_init()`, which * fill every field that size covers with its default: * * OtioReadOptions options; ``` | Parameter | Type | Role | | --- | --- | --- | | `out_options` | `OtioWriteOptions` | output | #### `write_to_bytes` `otio_write_to_bytes` · method Writes a document as the bytes of a file in some format. The buffer is NUL-terminated, so a text format's output can be used as a C string; `len` is what matters for a binary one. The bundle formats are refused with `OTIO_STATUS_UNSUPPORTED`: write one to a path. ```c OtioStatus otio_write_to_bytes( OtioFormat format, const OtioDocument *source, const OtioWriteOptions *options, OtioBuffer *out_bytes, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `format` | `OtioFormat` | input | | `source` (optional) | `document` | document in | | `options` (optional) | `OtioWriteOptions` | input | | `out_bytes` | `bytes` | output | | `out_error` | `text` | error | #### `write_to_file` `otio_write_to_file` · method Writes a document to a file on disk in some format. A bundle is written from the document's root, which has to be a timeline, along with a copy of every media file it references; an `.otiod` is a directory, and `path` names it. Neither overwrites: a bundle whose `path` already exists is refused. A build without a file system, such as one for WebAssembly, reports `OTIO_STATUS_UNSUPPORTED`; use [`otio_write_to_bytes`] there. ```c * otio_write_to_file(OTIO_FORMAT_OTIO_JSON, document, "cut.otio", NULL, NULL); ``` | Parameter | Type | Role | | --- | --- | --- | | `format` | `OtioFormat` | input | | `source` (optional) | `document` | document in | | `path` | `text` | input | | `options` (optional) | `OtioWriteOptions` | input | | `out_error` | `text` | error | ### Algorithm > Operations that build a new object out of existing ones. Symbols in this group are spelled `otio_algorithm_…`. An SDK drops that prefix. #### `flatten_stack` `otio_algorithm_flatten_stack` · method Collapses a stack's tracks into one, top layer winning where it is visible. ```c OtioStatus otio_algorithm_flatten_stack( OtioDocument *target, OtioNode stack, OtioNode *out_track, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `stack` | `node` | input | | `out_track` | `node` | output | | `out_error` | `text` | error | #### `flatten_tracks` `otio_algorithm_flatten_tracks` · method Collapses a list of tracks into one, lowest first. ```c OtioStatus otio_algorithm_flatten_tracks( OtioDocument *target, const OtioNode *tracks, size_t count, OtioNode *out_track, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `tracks` (optional) | `list` | input | | `count` | `size` | length | | `out_track` | `node` | output | | `out_error` | `text` | error | #### `track_trimmed_to_range` `otio_algorithm_track_trimmed_to_range` · method Returns a copy of a track holding only what falls inside a span. The copy is added to the same document and has no parent. ```c OtioStatus otio_algorithm_track_trimmed_to_range( OtioDocument *target, OtioNode track, OtioTimeRange trim_range, OtioNode *out_track, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `track` | `node` | input | | `trim_range` | `OtioTimeRange` | input | | `out_track` | `node` | output | | `out_error` | `text` | error | ### Clip > A piece of media, trimmed to the part an edit uses. Symbols in this group are spelled `otio_clip_…`. An SDK drops that prefix. #### `active_media_reference_key` `otio_clip_active_media_reference_key` · getter Returns which of a clip's media references is in use. ```c OtioStatus otio_clip_active_media_reference_key( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_key, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_key` | `text` | output | | `out_error` | `text` | error | #### `media_reference` `otio_clip_media_reference` · method · may answer nothing Returns one of a clip's media references. A null `key` means the active one. Reports `OTIO_STATUS_NO_VALUE` if the key names nothing, which for the active key is a clip with no media. ```c OtioStatus otio_clip_media_reference( const OtioDocument *source, OtioNode node_handle, const char *key, OtioNode *out_reference, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `key` (optional) | `text` | input | | `out_reference` | `node` | output | | `out_error` | `text` | error | #### `media_reference_count` `otio_clip_media_reference_count` · getter Returns how many media references a clip holds. ```c OtioStatus otio_clip_media_reference_count( const OtioDocument *source, OtioNode node_handle, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_count` | `size` | output | | `out_error` | `text` | error | #### `media_reference_key_at` `otio_clip_media_reference_key_at` · method Returns the key of one of a clip's media references. The references are ordered by key, so walking the indices walks them in a stable order. ```c OtioStatus otio_clip_media_reference_key_at( const OtioDocument *source, OtioNode node_handle, size_t index, OtioBuffer *out_key, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `index` | `size` | input | | `out_key` | `text` | output | | `out_error` | `text` | error | #### `new` `otio_clip_new` · constructor Creates a clip. `name` may be null for an unnamed one. ```c OtioStatus otio_clip_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `remove_media_reference` `otio_clip_remove_media_reference` · method · may answer nothing Removes one of a clip's media references, and returns it. ```c OtioStatus otio_clip_remove_media_reference( OtioDocument *target, OtioNode node_handle, const char *key, OtioNode *out_reference, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `key` | `text` | input | | `out_reference` | `node` | output | | `out_error` | `text` | error | #### `set_active_media_reference_key` `otio_clip_set_active_media_reference_key` · setter Sets which of a clip's media references is in use. ```c OtioStatus otio_clip_set_active_media_reference_key( OtioDocument *target, OtioNode node_handle, const char *key, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `key` | `text` | input | | `out_error` | `text` | error | #### `set_media_reference` `otio_clip_set_media_reference` · setter Sets one of a clip's media references, adding it if the key is new. ```c OtioStatus otio_clip_set_media_reference( OtioDocument *target, OtioNode node_handle, const char *key, OtioNode reference, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `key` | `text` | input | | `reference` | `node` | input | | `out_error` | `text` | error | ### Composable > Anything that can sit inside a composition. Symbols in this group are spelled `otio_composable_…`. An SDK drops that prefix. #### `new` `otio_composable_new` · constructor Creates a composable: something that sits in a composition and nothing more. ```c OtioStatus otio_composable_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | ### Composition > An item that holds other items and says where they sit. Symbols in this group are spelled `otio_composition_…`. An SDK drops that prefix. #### `append_child` `otio_composition_append_child` · method Adds a child to the end of a composition. ```c OtioStatus otio_composition_append_child( OtioDocument *target, OtioNode parent, OtioNode child, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `parent` | `node` | receiver | | `child` | `node` | input | | `out_error` | `text` | error | #### `child_at_time` `otio_composition_child_at_time` · method · may answer nothing Returns the child of a composition that covers an instant. `shallow` stops at the composition's own children rather than descending into nested ones. Reports `OTIO_STATUS_NO_VALUE` when nothing covers it. ```c OtioStatus otio_composition_child_at_time( const OtioDocument *source, OtioNode parent, OtioRationalTime time, bool shallow, OtioNode *out_child, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `time` | `OtioRationalTime` | input | | `shallow` | `bool` | input | | `out_child` | `node` | output | | `out_error` | `text` | error | #### `children_in_range` `otio_composition_children_in_range` · method Returns the children of a composition that touch a span. ```c OtioStatus otio_composition_children_in_range( const OtioDocument *source, OtioNode parent, OtioTimeRange search_range, OtioNode *out_nodes, size_t capacity, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `search_range` | `OtioTimeRange` | input | | `out_nodes` | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `clear_children` `otio_composition_clear_children` · clearer Removes every child of a composition, and returns them. ```c OtioStatus otio_composition_clear_children( OtioDocument *target, OtioNode parent, OtioNode *out_nodes, size_t capacity, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `parent` | `node` | receiver | | `out_nodes` | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `detach_child` `otio_composition_detach_child` · method Removes a child by handle. ```c OtioStatus otio_composition_detach_child( OtioDocument *target, OtioNode parent, OtioNode child, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `parent` | `node` | receiver | | `child` | `node` | input | | `out_error` | `text` | error | #### `find_children_of_kind` `otio_composition_find_children_of_kind` · method Returns every object of a kind at or below a composition. A null `search_range` searches all of it. `shallow` stops the walk at the composition's own children. ```c OtioStatus otio_composition_find_children_of_kind( const OtioDocument *source, OtioNode parent, OtioNodeKind kind, const OtioTimeRange *search_range, bool shallow, OtioNode *out_nodes, size_t capacity, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `kind` | `OtioNodeKind` | input | | `search_range` (optional) | `OtioTimeRange` | input | | `shallow` | `bool` | input | | `out_nodes` | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `handles_of_child` `otio_composition_handles_of_child` · method Returns how much unused media a child has on each side. ```c OtioStatus otio_composition_handles_of_child( const OtioDocument *source, OtioNode parent, OtioNode child, OtioHandles *out_handles, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `child` | `node` | input | | `out_handles` | `OtioHandles` | output | | `out_error` | `text` | error | #### `has_child` `otio_composition_has_child` · method Returns whether a composition holds an object directly. ```c OtioStatus otio_composition_has_child( const OtioDocument *source, OtioNode parent, OtioNode child, bool *out_has, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `child` | `node` | input | | `out_has` | `bool` | output | | `out_error` | `text` | error | #### `index_of_child` `otio_composition_index_of_child` · method Returns where a child sits in its composition. ```c OtioStatus otio_composition_index_of_child( const OtioDocument *source, OtioNode parent, OtioNode child, size_t *out_index, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `child` | `node` | input | | `out_index` | `size` | output | | `out_error` | `text` | error | #### `insert_child` `otio_composition_insert_child` · method Adds a child to a composition at an index. A negative index counts from the end, as upstream's Python does. The child must not already be in a composition. ```c OtioStatus otio_composition_insert_child( OtioDocument *target, OtioNode parent, int64_t index, OtioNode child, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `parent` | `node` | receiver | | `index` | `int64` | input | | `child` | `node` | input | | `out_error` | `text` | error | #### `is_parent_of` `otio_composition_is_parent_of` · method Returns whether an object descends from a composition at any depth. ```c OtioStatus otio_composition_is_parent_of( const OtioDocument *source, OtioNode parent, OtioNode other, bool *out_is, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `other` | `node` | input | | `out_is` | `bool` | output | | `out_error` | `text` | error | #### `neighbors_of` `otio_composition_neighbors_of` · method Returns the children on either side of one, or `otio_node_none` where there is none. With `OTIO_NEIGHBOR_GAP_AROUND_TRANSITIONS`, a transition at the head or tail of a track gets a gap materialized for its overhang, which is why this takes a document it may edit. ```c OtioStatus otio_composition_neighbors_of( OtioDocument *target, OtioNode parent, OtioNode child, OtioNeighborGapPolicy policy, OtioNode *out_before, OtioNode *out_after, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `parent` | `node` | receiver | | `child` | `node` | input | | `policy` | `OtioNeighborGapPolicy` | input | | `out_before` | `node` | output | | `out_after` | `node` | output | | `out_error` | `text` | error | #### `new` `otio_composition_new` · constructor Creates a bare composition: children with no layout of its own. ```c OtioStatus otio_composition_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `range_of_child` `otio_composition_range_of_child` · method Returns where a child sits in a composition's clock, at any depth. ```c OtioStatus otio_composition_range_of_child( const OtioDocument *source, OtioNode parent, OtioNode child, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `child` | `node` | input | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `range_of_child_at_index` `otio_composition_range_of_child_at_index` · method Returns where the child at an index sits in its composition's clock. A negative index counts from the end. ```c OtioStatus otio_composition_range_of_child_at_index( const OtioDocument *source, OtioNode parent, int64_t index, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `index` | `int64` | input | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `ranges_of_children` `otio_composition_ranges_of_children` · method Returns where every child of a composition sits, in one pass. `out_nodes` and `out_ranges` are filled in step, so entry `i` of one goes with entry `i` of the other. Either may be null when only the count is wanted. ```c OtioStatus otio_composition_ranges_of_children( const OtioDocument *source, OtioNode parent, OtioNode *out_nodes, OtioTimeRange *out_ranges, size_t capacity, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `out_nodes` (optional) | `list` | output list | | `out_ranges` (optional) | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `remove_child` `otio_composition_remove_child` · method Removes a child by index, and returns it. The child stays in the document with no parent. ```c OtioStatus otio_composition_remove_child( OtioDocument *target, OtioNode parent, int64_t index, OtioNode *out_child, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `parent` | `node` | receiver | | `index` | `int64` | input | | `out_child` | `node` | output | | `out_error` | `text` | error | #### `trim_child_range` `otio_composition_trim_child_range` · method · may answer nothing Clips a range to a composition's own trim. Reports `OTIO_STATUS_NO_VALUE` when nothing of it is left. ```c OtioStatus otio_composition_trim_child_range( const OtioDocument *source, OtioNode parent, OtioTimeRange child_range, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `child_range` | `OtioTimeRange` | input | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `trimmed_range_of_child` `otio_composition_trimmed_range_of_child` · method · may answer nothing Returns where a child sits after the composition's trim, at any depth. Reports `OTIO_STATUS_NO_VALUE` when the trim excludes it entirely. ```c OtioStatus otio_composition_trimmed_range_of_child( const OtioDocument *source, OtioNode parent, OtioNode child, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `child` | `node` | input | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `trimmed_range_of_child_at_index` `otio_composition_trimmed_range_of_child_at_index` · method Returns where the child at an index sits, after the composition's own trim. ```c OtioStatus otio_composition_trimmed_range_of_child_at_index( const OtioDocument *source, OtioNode parent, int64_t index, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `index` | `int64` | input | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | ### Document > The arena that owns every object in a timeline. Symbols in this group are spelled `otio_document_…`. An SDK drops that prefix. #### `absorb` `otio_document_absorb` · method Moves every object out of one document into another. This is the call that lets a binding offer the API OpenTimelineIO's own Python and C++ users expect, where a `Clip` is built on its own and put inside a `Track` afterwards. An arena has no way to do that with handles alone: a handle means nothing outside the document it was issued for, so an object built in one document has to be *moved* into the other rather than pointed at. Upstream's reference-counted object model gets this for free and pays for it elsewhere; see `docs/adr/0001-ownership-model.md`. `source` is consumed. On success it is released and the caller's pointer is set to null, so there is nothing left to free and no way to free it twice. On failure nothing moves and the pointer is left alone. A document cannot absorb itself: a `source` that points at `target` is `OTIO_STATUS_INVALID_ARGUMENT`. The source's root is not adopted, because the target has its own. # The translation Every object arrives under a new handle, so every handle the caller still holds into `source` has to be translated. The call fills `out_from` and `out_to` with the old handle and the new one for each object moved, in the same order, and writes how many pairs there are to `out_count`. Unlike the other calls that answer with a list, this one cannot be asked twice to size the buffer: the first call would already have consumed the source. Size the arrays with [`otio_document_node_count`] on the source before calling, which is exactly how many objects will move. A capacity smaller than that is `OTIO_STATUS_INVALID_ARGUMENT`, and nothing moves. # Example ```c size_t moving = otio_document_node_count(clip_document); OtioNode *from = malloc(moving * sizeof(OtioNode)); OtioNode *to = malloc(moving * sizeof(OtioNode)); size_t moved = 0; otio_document_absorb(timeline_document, &clip_document, from, to, moving, &moved, std::ptr::null_mut()); /* `clip_document` is now NULL; `clip` is `to[i]` where `from[i]` is `clip`. */ ``` ```c * otio_document_absorb(timeline_document, &clip_document, from, to, moving, &moved); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `source` (optional) | `document` | document taken | | `out_from` (optional) | `list` | output list | | `out_to` (optional) | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `clone` `otio_document_clone` · getter Copies a document, objects and all. The copy keeps the arena's layout, so a handle into the original, passed with the copy, names the copy of the same object. The two documents are independent afterwards: an object added to or removed from one is not added to or removed from the other, and a handle issued by one after the copy means nothing in the other. ```c OtioStatus otio_document_clone( const OtioDocument *source, OtioDocument **out_document, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `out_document` | `document` | output | | `out_error` | `text` | error | #### `contains` `otio_document_contains` · method Returns whether a handle still names a live object. ```c bool otio_document_contains(const OtioDocument *source, OtioNode node); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node` | `node` | input | #### `deep_clone` `otio_document_deep_clone` · method Copies an object and everything it owns, into the same document. The copy has no parent, whatever the original had. ```c OtioStatus otio_document_deep_clone( OtioDocument *target, OtioNode node, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node` | `node` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `free` `otio_document_free` · plumbing Releases a document and every object in it. Passing null does nothing. Every handle into the document is stale afterwards; using one is a programming error this library cannot detect, because the arena it would be checked against is gone. ```c * otio_document_free(document); ``` | Parameter | Type | Role | | --- | --- | --- | | `document` (optional) | `document` | document mut | #### `from_json` `otio_document_from_json` · constructor Reads a document from OTIO JSON. ```c OtioStatus otio_document_from_json( const char *json, OtioDocument **out_document, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `json` | `text` | input | | `out_document` | `document` | output | | `out_error` | `text` | error | #### `new` `otio_document_new` · constructor Creates an empty document with no root. Returns null only if the allocation fails. Release it with [`otio_document_free`]. ```c OtioDocument *otio_document_new(void); ``` #### `node_count` `otio_document_node_count` · getter Returns how many live objects the document holds. ```c size_t otio_document_node_count(const OtioDocument *source); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | #### `read_otio_file` `otio_document_read_from_file` · constructor Reads a document from a `.otio` file on disk. A build without a file system, such as one for WebAssembly, reports `OTIO_STATUS_UNSUPPORTED`; use [`otio_document_from_json`] there. ```c OtioStatus otio_document_read_from_file( const char *path, OtioDocument **out_document, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `path` | `text` | input | | `out_document` | `document` | output | | `out_error` | `text` | error | #### `remove_node` `otio_document_remove` · method Removes one object from the document. Anything that referred to it still holds a handle, and that handle is now stale: a lookup fails rather than reaching whatever takes the slot next. To remove an object together with everything hanging off it, use [`otio_document_remove_recursive`]. ```c OtioStatus otio_document_remove( OtioDocument *target, OtioNode node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node` | `node` | input | | `out_error` | `text` | error | #### `remove_node_recursive` `otio_document_remove_recursive` · method Removes an object and everything it owns: children, markers, effects and media references. ```c OtioStatus otio_document_remove_recursive( OtioDocument *target, OtioNode node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node` | `node` | input | | `out_error` | `text` | error | #### `root` `otio_document_root` · getter · may answer nothing Returns the document's root object. Reports `OTIO_STATUS_NO_VALUE` for a document that has none, which is what a freshly created one is. ```c * otio_document_root(document, &timeline, NULL); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `set_root` `otio_document_set_root` · setter Sets the document's root object. Passing `otio_node_none` clears it. ```c OtioStatus otio_document_set_root( OtioDocument *target, OtioNode node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node` (optional) | `node` | input | | `out_error` | `text` | error | #### `to_json` `otio_document_to_json` · method Writes a document as OTIO JSON, starting from its root. `indent` is how many spaces each level is indented by; [`otio_default_indent`] is what upstream's Python bindings use. ```c OtioStatus otio_document_to_json( const OtioDocument *source, size_t indent, OtioBuffer *out_json, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `indent` | `size` | input | | `out_json` | `text` | output | | `out_error` | `text` | error | #### `write_otio_file` `otio_document_write_to_file` · method Writes a document to a `.otio` file on disk. A build without a file system, such as one for WebAssembly, reports `OTIO_STATUS_UNSUPPORTED`; use [`otio_document_to_json`] there. ```c OtioStatus otio_document_write_to_file( const OtioDocument *source, const char *path, size_t indent, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `path` | `text` | input | | `indent` | `size` | input | | `out_error` | `text` | error | ### Edit > The ten edit operations, as an editor's tools spell them. Symbols in this group are spelled `otio_edit_…`. An SDK drops that prefix. #### `fill` `otio_edit_fill` · method Drops an item into a gap on a track, fitting it as the reference point says. ```c OtioStatus otio_edit_fill( OtioDocument *target, OtioNode item, OtioNode track, OtioRationalTime track_time, OtioReferencePoint reference_point, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `item` | `node` | input | | `track` | `node` | input | | `track_time` | `OtioRationalTime` | input | | `reference_point` | `OtioReferencePoint` | input | | `out_error` | `text` | error | #### `insert` `otio_edit_insert` · method Inserts an item at an instant, pushing what follows later. ```c OtioStatus otio_edit_insert( OtioDocument *target, OtioNode item, OtioNode composition, OtioRationalTime time, bool remove_transitions, OtioNode fill_template, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `item` | `node` | input | | `composition` | `node` | input | | `time` | `OtioRationalTime` | input | | `remove_transitions` | `bool` | input | | `fill_template` (optional) | `node` | input | | `out_error` | `text` | error | #### `overwrite` `otio_edit_overwrite` · method Lays an item over a span of a composition, replacing what was there. `fill_template` is the item to fill any gap the edit opens with, or `otio_node_none` for a plain gap. ```c OtioStatus otio_edit_overwrite( OtioDocument *target, OtioNode item, OtioNode composition, OtioTimeRange range, bool remove_transitions, OtioNode fill_template, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `item` | `node` | input | | `composition` | `node` | input | | `range` | `OtioTimeRange` | input | | `remove_transitions` | `bool` | input | | `fill_template` (optional) | `node` | input | | `out_error` | `text` | error | #### `remove` `otio_edit_remove` · method Takes whatever sits at an instant out of a composition. With `fill` set, a gap takes its place; without, what follows moves up. ```c OtioStatus otio_edit_remove( OtioDocument *target, OtioNode composition, OtioRationalTime time, bool fill, OtioNode fill_template, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `composition` | `node` | input | | `time` | `OtioRationalTime` | input | | `fill` | `bool` | input | | `fill_template` (optional) | `node` | input | | `out_error` | `text` | error | #### `ripple` `otio_edit_ripple` · method Moves an item's in and out points, sliding everything after it. ```c OtioStatus otio_edit_ripple( OtioDocument *target, OtioNode item, OtioRationalTime delta_in, OtioRationalTime delta_out, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `item` | `node` | input | | `delta_in` | `OtioRationalTime` | input | | `delta_out` | `OtioRationalTime` | input | | `out_error` | `text` | error | #### `roll` `otio_edit_roll` · method Moves the cut between an item and its neighbour. ```c OtioStatus otio_edit_roll( OtioDocument *target, OtioNode item, OtioRationalTime delta_in, OtioRationalTime delta_out, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `item` | `node` | input | | `delta_in` | `OtioRationalTime` | input | | `delta_out` | `OtioRationalTime` | input | | `out_error` | `text` | error | #### `slice` `otio_edit_slice` · method Cuts whatever sits at an instant into two. ```c OtioStatus otio_edit_slice( OtioDocument *target, OtioNode composition, OtioRationalTime time, bool remove_transitions, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `composition` | `node` | input | | `time` | `OtioRationalTime` | input | | `remove_transitions` | `bool` | input | | `out_error` | `text` | error | #### `slide` `otio_edit_slide` · method Moves an item along its track, taking the time from its neighbours. ```c OtioStatus otio_edit_slide( OtioDocument *target, OtioNode item, OtioRationalTime delta, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `item` | `node` | input | | `delta` | `OtioRationalTime` | input | | `out_error` | `text` | error | #### `slip` `otio_edit_slip` · method Moves the media inside an item without moving the item. ```c OtioStatus otio_edit_slip( OtioDocument *target, OtioNode item, OtioRationalTime delta, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `item` | `node` | input | | `delta` | `OtioRationalTime` | input | | `out_error` | `text` | error | #### `trim` `otio_edit_trim` · method Moves an item's in and out points without moving its neighbours. ```c OtioStatus otio_edit_trim( OtioDocument *target, OtioNode item, OtioRationalTime delta_in, OtioRationalTime delta_out, OtioNode fill_template, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `item` | `node` | input | | `delta_in` | `OtioRationalTime` | input | | `delta_out` | `OtioRationalTime` | input | | `fill_template` (optional) | `node` | input | | `out_error` | `text` | error | ### Effect > Something applied to an item that changes how it plays. Symbols in this group are spelled `otio_effect_…`. An SDK drops that prefix. #### `effect_name` `otio_effect_effect_name` · getter Returns an effect's own name, such as `"LinearTimeWarp"`. ```c OtioStatus otio_effect_effect_name( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_name` | `text` | output | | `out_error` | `text` | error | #### `enabled` `otio_effect_enabled` · getter Returns whether an effect is applied. ```c OtioStatus otio_effect_enabled( const OtioDocument *source, OtioNode node_handle, bool *out_enabled, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_enabled` | `bool` | output | | `out_error` | `text` | error | #### `new` `otio_effect_new` · constructor Creates an effect. `effect_name` is the effect's own name, such as `"Blur"`, which is separate from the object's name. ```c OtioStatus otio_effect_new( OtioDocument *target, const char *name, const char *effect_name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `effect_name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `set_effect_name` `otio_effect_set_effect_name` · setter Sets an effect's own name. ```c OtioStatus otio_effect_set_effect_name( OtioDocument *target, OtioNode node_handle, const char *effect_name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `effect_name` | `text` | input | | `out_error` | `text` | error | #### `set_enabled` `otio_effect_set_enabled` · setter Sets whether an effect is applied. ```c OtioStatus otio_effect_set_enabled( OtioDocument *target, OtioNode node_handle, bool enabled, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `enabled` | `bool` | input | | `out_error` | `text` | error | #### `set_time_scalar` `otio_effect_set_time_scalar` · setter Sets a speed change's multiplier. ```c OtioStatus otio_effect_set_time_scalar( OtioDocument *target, OtioNode node_handle, double scalar, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `scalar` | `double` | input | | `out_error` | `text` | error | #### `time_scalar` `otio_effect_time_scalar` · getter Returns a speed change's multiplier. Only a `LinearTimeWarp` and a `FreezeFrame` have one; anything else reports `OTIO_STATUS_WRONG_KIND`. ```c OtioStatus otio_effect_time_scalar( const OtioDocument *source, OtioNode node_handle, double *out_scalar, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_scalar` | `double` | output | | `out_error` | `text` | error | ### Extensions > Turning on extensions: work built in otio-rust that is not part of OpenTimelineIO. Symbols in this group are spelled `otio_ext_…`. An SDK drops that prefix. #### `enable_extension` `otio_ext_enable` · free Turns on the extension `id`, and every extension it depends on first. Enabling one that is already on does nothing and succeeds. An id this library was not built with is `OTIO_STATUS_INVALID_ARGUMENT`, naming the extensions it was built with; an extension that fails to register is `OTIO_STATUS_PLUGIN_ERROR`. ```c OtioStatus otio_ext_enable(const char *id, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `id` | `text` | input | | `out_error` | `text` | error | #### `is_extension_enabled` `otio_ext_is_enabled` · free Writes whether the extension `id` is on to `out_enabled`. An id this library was not built with is simply not on. ```c OtioStatus otio_ext_is_enabled( const char *id, bool *out_enabled, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `id` | `text` | input | | `out_enabled` | `bool` | output | | `out_error` | `text` | error | ### ExternalReference > Media at a URL. Symbols in this group are spelled `otio_external_reference_…`. An SDK drops that prefix. #### `new` `otio_external_reference_new` · constructor Creates a media reference pointing at a URL. ```c OtioStatus otio_external_reference_new( OtioDocument *target, const char *name, const char *target_url, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `target_url` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `set_target_url` `otio_external_reference_set_target_url` · setter Sets where an external reference's media lives. ```c OtioStatus otio_external_reference_set_target_url( OtioDocument *target, OtioNode node_handle, const char *url, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `url` | `text` | input | | `out_error` | `text` | error | #### `target_url` `otio_external_reference_target_url` · getter Returns where an external reference's media lives. ```c OtioStatus otio_external_reference_target_url( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_url, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_url` | `text` | output | | `out_error` | `text` | error | ### Format > The file formats this library reads and writes. Symbols in this group are spelled `otio_format_…`. An SDK drops that prefix. #### `from_suffix` `otio_format_from_suffix` · constructor · may answer nothing Returns the format that claims a filename suffix, such as `"edl"`. The suffix is matched without its dot and without regard to case. Reports `OTIO_STATUS_NO_VALUE` for a suffix no format claims. A build for WebAssembly, which has no file system, claims neither `otioz` nor `otiod`, since it cannot read or write either. ```c OtioStatus otio_format_from_suffix( const char *suffix, OtioFormat *out_format, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `suffix` | `text` | input | | `out_format` | `OtioFormat` | output | | `out_error` | `text` | error | #### `name` `otio_format_name` · getter Returns an adapter's name, as upstream's plugin manifest spells it. The string is static and needs no freeing. ```c const char *otio_format_name(OtioFormat format); ``` | Parameter | Type | Role | | --- | --- | --- | | `format` | `OtioFormat` | receiver | ### FreezeFrame > A hold on a single frame. Symbols in this group are spelled `otio_freeze_frame_…`. An SDK drops that prefix. #### `new` `otio_freeze_frame_new` · constructor Creates a freeze frame: a hold on a single frame. ```c OtioStatus otio_freeze_frame_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | ### Gap > Time that holds nothing. Symbols in this group are spelled `otio_gap_…`. An SDK drops that prefix. #### `new` `otio_gap_new` · constructor Creates a gap. ```c OtioStatus otio_gap_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | ### GeneratorReference > Media a generator produces, such as colour bars. Symbols in this group are spelled `otio_generator_reference_…`. An SDK drops that prefix. #### `generator_kind` `otio_generator_reference_kind` · getter Returns which generator a generator reference names, such as `"SMPTEBars"`. ```c OtioStatus otio_generator_reference_kind( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_kind, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_kind` | `text` | output | | `out_error` | `text` | error | #### `new` `otio_generator_reference_new` · constructor Creates a media reference for generated media, such as colour bars. ```c OtioStatus otio_generator_reference_new( OtioDocument *target, const char *name, const char *generator_kind, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `generator_kind` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `set_generator_kind` `otio_generator_reference_set_kind` · setter Sets which generator a generator reference names. ```c OtioStatus otio_generator_reference_set_kind( OtioDocument *target, OtioNode node_handle, const char *kind, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `kind` | `text` | input | | `out_error` | `text` | error | ### ImageSequenceReference > Media held as numbered frames on disk. Symbols in this group are spelled `otio_image_sequence_reference_…`. An SDK drops that prefix. #### `name_prefix` `otio_image_sequence_reference_name_prefix` · getter Returns the part of each frame's filename before the frame number. ```c OtioStatus otio_image_sequence_reference_name_prefix( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_prefix, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_prefix` | `text` | output | | `out_error` | `text` | error | #### `name_suffix` `otio_image_sequence_reference_name_suffix` · getter Returns the part of each frame's filename after the frame number. ```c OtioStatus otio_image_sequence_reference_name_suffix( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_suffix, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_suffix` | `text` | output | | `out_error` | `text` | error | #### `new` `otio_image_sequence_reference_new` · constructor Creates a media reference for a numbered sequence of image files. The filename parts and the numbers start empty and at zero; set them with [`otio_image_sequence_reference_set_numbers`] and the calls beside it. ```c OtioStatus otio_image_sequence_reference_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `numbers` `otio_image_sequence_reference_numbers` · getter Returns the numbers describing how an image sequence is laid out. ```c OtioStatus otio_image_sequence_reference_numbers( const OtioDocument *source, OtioNode node_handle, OtioImageSequence *out_numbers, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_numbers` | `OtioImageSequence` | output | | `out_error` | `text` | error | #### `set_name_prefix` `otio_image_sequence_reference_set_name_prefix` · setter Sets the part of each frame's filename before the frame number. ```c OtioStatus otio_image_sequence_reference_set_name_prefix( OtioDocument *target, OtioNode node_handle, const char *prefix, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `prefix` | `text` | input | | `out_error` | `text` | error | #### `set_name_suffix` `otio_image_sequence_reference_set_name_suffix` · setter Sets the part of each frame's filename after the frame number. ```c OtioStatus otio_image_sequence_reference_set_name_suffix( OtioDocument *target, OtioNode node_handle, const char *suffix, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `suffix` | `text` | input | | `out_error` | `text` | error | #### `set_numbers` `otio_image_sequence_reference_set_numbers` · setter Sets the numbers describing how an image sequence is laid out. ```c OtioStatus otio_image_sequence_reference_set_numbers( OtioDocument *target, OtioNode node_handle, OtioImageSequence numbers, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `numbers` | `OtioImageSequence` | input | | `out_error` | `text` | error | #### `set_target_url_base` `otio_image_sequence_reference_set_target_url_base` · setter Sets the directory an image sequence's frames sit in. ```c OtioStatus otio_image_sequence_reference_set_target_url_base( OtioDocument *target, OtioNode node_handle, const char *url_base, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `url_base` | `text` | input | | `out_error` | `text` | error | #### `target_url_base` `otio_image_sequence_reference_target_url_base` · getter Returns the directory an image sequence's frames sit in. ```c OtioStatus otio_image_sequence_reference_target_url_base( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_url_base, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_url_base` | `text` | output | | `out_error` | `text` | error | ### Item > Anything that occupies time. Symbols in this group are spelled `otio_item_…`. An SDK drops that prefix. #### `append_effect` `otio_item_append_effect` · method Adds an effect to an item. Effects apply in the order they are added. ```c OtioStatus otio_item_append_effect( OtioDocument *target, OtioNode node_handle, OtioNode effect_handle, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `effect_handle` | `node` | input | | `out_error` | `text` | error | #### `append_marker` `otio_item_append_marker` · method Adds a marker to an item. ```c OtioStatus otio_item_append_marker( OtioDocument *target, OtioNode node_handle, OtioNode marker_handle, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `marker_handle` | `node` | input | | `out_error` | `text` | error | #### `available_range` `otio_item_available_range` · getter Returns the span of media an object could draw on, before trimming. ```c OtioStatus otio_item_available_range( const OtioDocument *source, OtioNode node_handle, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `clear_color` `otio_item_clear_color` · clearer Clears an item's display tint. ```c OtioStatus otio_item_clear_color( OtioDocument *target, OtioNode node_handle, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `out_error` | `text` | error | #### `clear_source_range` `otio_item_clear_source_range` · clearer Clears an item's source range, so that it takes all of its media. ```c OtioStatus otio_item_clear_source_range( OtioDocument *target, OtioNode node_handle, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `out_error` | `text` | error | #### `color` `otio_item_color` · method · may answer nothing Returns an item's display tint, and the name that goes with it. `out_name` may be null if the name is not wanted. Reports `OTIO_STATUS_NO_VALUE` for an untinted item. ```c OtioStatus otio_item_color( const OtioDocument *source, OtioNode node_handle, OtioColor *out_color, OtioBuffer *out_name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_color` | `OtioColor` | output | | `out_name` (optional) | `text` | output | | `out_error` | `text` | error | #### `duration` `otio_item_duration` · getter Returns how long an object occupies its parent's timeline. ```c OtioStatus otio_item_duration( const OtioDocument *source, OtioNode node_handle, OtioRationalTime *out_duration, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_duration` | `OtioRationalTime` | output | | `out_error` | `text` | error | #### `effect_at` `otio_item_effect_at` · method Returns one of an item's effects. ```c OtioStatus otio_item_effect_at( const OtioDocument *source, OtioNode node_handle, size_t index, OtioNode *out_effect, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `index` | `size` | input | | `out_effect` | `node` | output | | `out_error` | `text` | error | #### `effect_count` `otio_item_effect_count` · getter Returns how many effects an item carries. ```c OtioStatus otio_item_effect_count( const OtioDocument *source, OtioNode node_handle, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_count` | `size` | output | | `out_error` | `text` | error | #### `enabled` `otio_item_enabled` · getter Returns whether an item contributes to its composition. ```c OtioStatus otio_item_enabled( const OtioDocument *source, OtioNode node_handle, bool *out_enabled, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_enabled` | `bool` | output | | `out_error` | `text` | error | #### `marker_at` `otio_item_marker_at` · method Returns one of an item's markers. ```c OtioStatus otio_item_marker_at( const OtioDocument *source, OtioNode node_handle, size_t index, OtioNode *out_marker, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `index` | `size` | input | | `out_marker` | `node` | output | | `out_error` | `text` | error | #### `marker_count` `otio_item_marker_count` · getter Returns how many markers an item carries. ```c OtioStatus otio_item_marker_count( const OtioDocument *source, OtioNode node_handle, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_count` | `size` | output | | `out_error` | `text` | error | #### `new` `otio_item_new` · constructor Creates a bare item: something that occupies time without saying what fills it. ```c OtioStatus otio_item_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `range_in_parent` `otio_item_range_in_parent` · getter Returns where an object sits in its parent's clock. ```c OtioStatus otio_item_range_in_parent( const OtioDocument *source, OtioNode node_handle, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `remove_effect` `otio_item_remove_effect` · method Removes one of an item's effects, and returns it. ```c OtioStatus otio_item_remove_effect( OtioDocument *target, OtioNode node_handle, size_t index, OtioNode *out_effect, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `index` | `size` | input | | `out_effect` | `node` | output | | `out_error` | `text` | error | #### `remove_marker` `otio_item_remove_marker` · method Removes one of an item's markers, and returns it. The marker stays in the document; remove it with `otio_document_remove_recursive` if nothing else holds it. ```c OtioStatus otio_item_remove_marker( OtioDocument *target, OtioNode node_handle, size_t index, OtioNode *out_marker, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `index` | `size` | input | | `out_marker` | `node` | output | | `out_error` | `text` | error | #### `set_color` `otio_item_set_color` · setter Sets an item's display tint. `name` may be null for an unnamed colour. ```c OtioStatus otio_item_set_color( OtioDocument *target, OtioNode node_handle, OtioColor color, const char *name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `color` | `OtioColor` | input | | `name` (optional) | `text` | input | | `out_error` | `text` | error | #### `set_enabled` `otio_item_set_enabled` · setter Sets whether an item contributes to its composition. ```c OtioStatus otio_item_set_enabled( OtioDocument *target, OtioNode node_handle, bool enabled, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `enabled` | `bool` | input | | `out_error` | `text` | error | #### `set_source_range` `otio_item_set_source_range` · setter Sets the portion of its media an item uses. ```c OtioStatus otio_item_set_source_range( OtioDocument *target, OtioNode node_handle, OtioTimeRange range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `range` | `OtioTimeRange` | input | | `out_error` | `text` | error | #### `source_range` `otio_item_source_range` · getter · may answer nothing Returns the portion of its media an item uses. Reports `OTIO_STATUS_NO_VALUE` for an item that takes all of it. ```c OtioStatus otio_item_source_range( const OtioDocument *source, OtioNode node_handle, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `trimmed_range` `otio_item_trimmed_range` · getter Returns the span of media an object uses, in its own clock. ```c OtioStatus otio_item_trimmed_range( const OtioDocument *source, OtioNode node_handle, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `trimmed_range_in_parent` `otio_item_trimmed_range_in_parent` · getter · may answer nothing Returns where an object sits in its parent's clock, after the parent's own trim. Reports `OTIO_STATUS_NO_VALUE` when the parent's trim excludes the object entirely. ```c OtioStatus otio_item_trimmed_range_in_parent( const OtioDocument *source, OtioNode node_handle, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `visible_range` `otio_item_visible_range` · getter Returns the span of media an object shows, including what its transitions reach into. ```c OtioStatus otio_item_visible_range( const OtioDocument *source, OtioNode node_handle, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | ### Library > The library itself: its version, and the defaults it uses. Symbols in this group are spelled `otio_abi_…` or `otio_buffer_…` or `otio_default_…` or `otio_status_…` or `otio_version_…`. An SDK drops that prefix. #### `abi_version` `otio_abi_version` · free Returns the version of the interface the library was built with, which a caller compares with the `OTIO_ABI_VERSION` its header states. A library and a caller with different versions cannot be used together. ```c * which this header defines, and `otio_abi_version()`, which the library * returns, are the version of the binary interface, for programs: a caller * checks at startup that the two agree, and refuses to go on if they do not. * * The ABI version changes only when a program built against one header could * not run against a library built from the next: a signature, the layout of a * struct without a `struct_size`, or the meaning of an enum value changing, * or an entry point going away. Adding entry points, enum values, or fields * at the end of a struct with a `struct_size` does not change it. * * Version 0, the current one, says the ABI has not yet been declared stable: * any release may change it, so a caller should also insist on the exact * `otio_version()` it was built for. Declaring it stable makes it 1. * * From version 1, a packaged release names the shared library for its ABI * version, so that two incompatible versions can be installed side by side: * `libotio.so.1` (with `libotio.so` as the link-time name) on Linux, * `libotio.1.dylib` on macOS, and `otio1.dll` on Windows. * * * A WHOLE SESSION * * OtioDocument *document = NULL; ``` #### `buffer_free` `otio_buffer_free` · plumbing Releases a buffer the library handed out. Passing a buffer whose `data` is null does nothing. Passing the same buffer twice, or one this library did not produce, is undefined. ```c * otio_buffer_free(error); ``` | Parameter | Type | Role | | --- | --- | --- | | `buffer` | `OtioBuffer` | input | #### `default_epsilon_s` `otio_default_epsilon_s` · free The tolerance, in seconds, that the range predicates use by default. Upstream's C++ takes this as a default argument; C has none, so the value is a call rather than a constant in the header, which keeps the two from drifting apart. ```c double otio_default_epsilon_s(void); ``` #### `default_indent` `otio_default_indent` · free Returns the indentation upstream's Python bindings write by default. ```c size_t otio_default_indent(void); ``` #### `status_name` `otio_status_name` · plumbing Returns the name of a status code, such as `"OTIO_STATUS_OK"`. The string is static and needs no freeing. ```c const char *otio_status_name(OtioStatus status); ``` | Parameter | Type | Role | | --- | --- | --- | | `status` | `OtioStatus` | input | #### `version` `otio_version` · free Returns the library's version, as `"MAJOR.MINOR.PATCH"`. The string is static and needs no freeing. ```c * `otio_version()` is the library's release, for people. `OTIO_ABI_VERSION`, * which this header defines, and `otio_abi_version()`, which the library * returns, are the version of the binary interface, for programs: a caller * checks at startup that the two agree, and refuses to go on if they do not. * * The ABI version changes only when a program built against one header could * not run against a library built from the next: a signature, the layout of a * struct without a `struct_size`, or the meaning of an enum value changing, * or an entry point going away. Adding entry points, enum values, or fields * at the end of a struct with a `struct_size` does not change it. * * Version 0, the current one, says the ABI has not yet been declared stable: * any release may change it, so a caller should also insist on the exact * `otio_version()` it was built for. Declaring it stable makes it 1. * * From version 1, a packaged release names the shared library for its ABI * version, so that two incompatible versions can be installed side by side: * `libotio.so.1` (with `libotio.so` as the link-time name) on Linux, * `libotio.1.dylib` on macOS, and `otio1.dll` on Windows. * * * A WHOLE SESSION * * OtioDocument *document = NULL; ``` ### LinearTimeWarp > A constant-rate speed change. Symbols in this group are spelled `otio_linear_time_warp_…`. An SDK drops that prefix. #### `new` `otio_linear_time_warp_new` · constructor Creates a constant-rate speed change. A `time_scalar` of 2.0 plays twice as fast. ```c OtioStatus otio_linear_time_warp_new( OtioDocument *target, const char *name, double time_scalar, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `time_scalar` | `double` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | ### Marker > A note pinned to a span of an item. Symbols in this group are spelled `otio_marker_…`. An SDK drops that prefix. #### `color` `otio_marker_color` · method · may answer nothing Returns a marker's tint, and the name that goes with it. `out_name` may be null if the name is not wanted. Reports `OTIO_STATUS_NO_VALUE` for an untinted marker. ```c OtioStatus otio_marker_color( const OtioDocument *source, OtioNode node_handle, OtioColor *out_color, OtioBuffer *out_name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_color` | `OtioColor` | output | | `out_name` (optional) | `text` | output | | `out_error` | `text` | error | #### `comment` `otio_marker_comment` · getter Returns a marker's note. ```c OtioStatus otio_marker_comment( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_comment, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_comment` | `text` | output | | `out_error` | `text` | error | #### `marked_range` `otio_marker_marked_range` · getter Returns the span a marker covers. ```c OtioStatus otio_marker_marked_range( const OtioDocument *source, OtioNode node_handle, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `new` `otio_marker_new` · constructor Creates a marker covering `marked_range`. ```c OtioStatus otio_marker_new( OtioDocument *target, const char *name, OtioTimeRange marked_range, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `marked_range` | `OtioTimeRange` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `set_color` `otio_marker_set_color` · setter Sets a marker's tint. `name` may be null for an unnamed colour. ```c OtioStatus otio_marker_set_color( OtioDocument *target, OtioNode node_handle, OtioColor color, const char *name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `color` | `OtioColor` | input | | `name` (optional) | `text` | input | | `out_error` | `text` | error | #### `set_comment` `otio_marker_set_comment` · setter Sets a marker's note. ```c OtioStatus otio_marker_set_comment( OtioDocument *target, OtioNode node_handle, const char *comment, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `comment` | `text` | input | | `out_error` | `text` | error | #### `set_marked_range` `otio_marker_set_marked_range` · setter Sets the span a marker covers. ```c OtioStatus otio_marker_set_marked_range( OtioDocument *target, OtioNode node_handle, OtioTimeRange range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `range` | `OtioTimeRange` | input | | `out_error` | `text` | error | ### MediaReference > What a clip draws its pictures from. Symbols in this group are spelled `otio_media_reference_…`. An SDK drops that prefix. #### `available_image_bounds` `otio_media_reference_available_image_bounds` · getter · may answer nothing Returns the image bounds a reference says its media has. ```c OtioStatus otio_media_reference_available_image_bounds( const OtioDocument *source, OtioNode node_handle, OtioBox2d *out_bounds, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_bounds` | `OtioBox2d` | output | | `out_error` | `text` | error | #### `available_range` `otio_media_reference_available_range` · getter · may answer nothing Returns the span of media a reference says is available. Reports `OTIO_STATUS_NO_VALUE` for a reference that does not say. ```c OtioStatus otio_media_reference_available_range( const OtioDocument *source, OtioNode node_handle, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `clear_available_image_bounds` `otio_media_reference_clear_available_image_bounds` · clearer Clears the image bounds a reference says its media has. ```c OtioStatus otio_media_reference_clear_available_image_bounds( OtioDocument *target, OtioNode node_handle, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `out_error` | `text` | error | #### `clear_available_range` `otio_media_reference_clear_available_range` · clearer Clears the span of media a reference says is available. ```c OtioStatus otio_media_reference_clear_available_range( OtioDocument *target, OtioNode node_handle, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `out_error` | `text` | error | #### `set_available_image_bounds` `otio_media_reference_set_available_image_bounds` · setter Sets the image bounds a reference says its media has. ```c OtioStatus otio_media_reference_set_available_image_bounds( OtioDocument *target, OtioNode node_handle, OtioBox2d bounds, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `bounds` | `OtioBox2d` | input | | `out_error` | `text` | error | #### `set_available_range` `otio_media_reference_set_available_range` · setter Sets the span of media a reference says is available. ```c OtioStatus otio_media_reference_set_available_range( OtioDocument *target, OtioNode node_handle, OtioTimeRange range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `range` | `OtioTimeRange` | input | | `out_error` | `text` | error | ### Metadata > The free-form dictionary every named object carries. Symbols in this group are spelled `otio_metadata_…`. An SDK drops that prefix. #### `clear` `otio_metadata_clear` · method Empties an object's metadata. ```c OtioStatus otio_metadata_clear( OtioDocument *target, OtioNode node_handle, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `out_error` | `text` | error | #### `contains` `otio_metadata_contains` · method Returns whether anything sits at a path. ```c OtioStatus otio_metadata_contains( const OtioDocument *source, OtioNode node_handle, const char *path, bool *out_contains, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_contains` | `bool` | output | | `out_error` | `text` | error | #### `get_bool` `otio_metadata_get_bool` · method Reads a boolean from the metadata. ```c OtioStatus otio_metadata_get_bool( const OtioDocument *source, OtioNode node_handle, const char *path, bool *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `bool` | output | | `out_error` | `text` | error | #### `get_box2d` `otio_metadata_get_box2d` · method Reads a rectangle from the metadata. ```c OtioStatus otio_metadata_get_box2d( const OtioDocument *source, OtioNode node_handle, const char *path, OtioBox2d *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `OtioBox2d` | output | | `out_error` | `text` | error | #### `get_color` `otio_metadata_get_color` · method Reads a colour, and the name that goes with it, from the metadata. `out_name` may be null if the name is not wanted. ```c OtioStatus otio_metadata_get_color( const OtioDocument *source, OtioNode node_handle, const char *path, OtioColor *out_value, OtioBuffer *out_name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `OtioColor` | output | | `out_name` (optional) | `text` | output | | `out_error` | `text` | error | #### `get_double` `otio_metadata_get_double` · method Reads a number from the metadata. ```c OtioStatus otio_metadata_get_double( const OtioDocument *source, OtioNode node_handle, const char *path, double *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `double` | output | | `out_error` | `text` | error | #### `get_int` `otio_metadata_get_int` · method Reads a signed integer from the metadata. ```c OtioStatus otio_metadata_get_int( const OtioDocument *source, OtioNode node_handle, const char *path, int64_t *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `int64` | output | | `out_error` | `text` | error | #### `get_object` `otio_metadata_get_object` · method Reads a handle to an OTIO object held in the metadata. ```c OtioStatus otio_metadata_get_object( const OtioDocument *source, OtioNode node_handle, const char *path, OtioNode *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `node` | output | | `out_error` | `text` | error | #### `get_rational_time` `otio_metadata_get_rational_time` · method Reads a time from the metadata. ```c OtioStatus otio_metadata_get_rational_time( const OtioDocument *source, OtioNode node_handle, const char *path, OtioRationalTime *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `OtioRationalTime` | output | | `out_error` | `text` | error | #### `get_string` `otio_metadata_get_string` · method Reads a string from the metadata. ```c OtioStatus otio_metadata_get_string( const OtioDocument *source, OtioNode node_handle, const char *path, OtioBuffer *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `text` | output | | `out_error` | `text` | error | #### `get_time_range` `otio_metadata_get_time_range` · method Reads a span from the metadata. ```c OtioStatus otio_metadata_get_time_range( const OtioDocument *source, OtioNode node_handle, const char *path, OtioTimeRange *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `get_time_transform` `otio_metadata_get_time_transform` · method Reads a transform from the metadata. ```c OtioStatus otio_metadata_get_time_transform( const OtioDocument *source, OtioNode node_handle, const char *path, OtioTimeTransform *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `OtioTimeTransform` | output | | `out_error` | `text` | error | #### `get_uint` `otio_metadata_get_uint` · method Reads an unsigned integer from the metadata. ```c OtioStatus otio_metadata_get_uint( const OtioDocument *source, OtioNode node_handle, const char *path, uint64_t *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `uint64` | output | | `out_error` | `text` | error | #### `get_v2d` `otio_metadata_get_v2d` · method Reads a point from the metadata. ```c OtioStatus otio_metadata_get_v2d( const OtioDocument *source, OtioNode node_handle, const char *path, OtioV2d *out_value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_value` | `OtioV2d` | output | | `out_error` | `text` | error | #### `key_at` `otio_metadata_key_at` · method · may answer nothing Returns the key at an index of a dictionary at a path. Keys are ordered, so walking the indices walks them the same way twice. ```c OtioStatus otio_metadata_key_at( const OtioDocument *source, OtioNode node_handle, const char *path, size_t index, OtioBuffer *out_key, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `index` | `size` | input | | `out_key` | `text` | output | | `out_error` | `text` | error | #### `kind` `otio_metadata_kind` · method · may answer nothing Returns what kind of value sits at a path. An empty or null path names the metadata dictionary itself, which is always `OTIO_VALUE_DICTIONARY`. ```c OtioStatus otio_metadata_kind( const OtioDocument *source, OtioNode node_handle, const char *path, OtioValueKind *out_kind, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_kind` | `OtioValueKind` | output | | `out_error` | `text` | error | #### `len` `otio_metadata_len` · method · may answer nothing Returns how many entries a dictionary or an array at a path holds. ```c OtioStatus otio_metadata_len( const OtioDocument *source, OtioNode node_handle, const char *path, size_t *out_len, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_len` | `size` | output | | `out_error` | `text` | error | #### `remove` `otio_metadata_remove` · method · may answer nothing Removes whatever sits at a path. Removing a key of a dictionary takes the key with it; removing an element of an array shortens the array. ```c OtioStatus otio_metadata_remove( OtioDocument *target, OtioNode node_handle, const char *path, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_error` | `text` | error | #### `set_bool` `otio_metadata_set_bool` · setter Writes a boolean into the metadata. ```c OtioStatus otio_metadata_set_bool( OtioDocument *target, OtioNode node_handle, const char *path, bool value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `bool` | input | | `out_error` | `text` | error | #### `set_box2d` `otio_metadata_set_box2d` · setter Writes a rectangle into the metadata. ```c OtioStatus otio_metadata_set_box2d( OtioDocument *target, OtioNode node_handle, const char *path, OtioBox2d value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `OtioBox2d` | input | | `out_error` | `text` | error | #### `set_color` `otio_metadata_set_color` · setter Writes a colour into the metadata. `name` may be null for an unnamed one. ```c OtioStatus otio_metadata_set_color( OtioDocument *target, OtioNode node_handle, const char *path, OtioColor value, const char *name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `OtioColor` | input | | `name` (optional) | `text` | input | | `out_error` | `text` | error | #### `set_dictionary` `otio_metadata_set_dictionary` · setter Writes an empty dictionary into the metadata, to be filled through deeper paths. ```c OtioStatus otio_metadata_set_dictionary( OtioDocument *target, OtioNode node_handle, const char *path, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_error` | `text` | error | #### `set_double` `otio_metadata_set_double` · setter Writes a number into the metadata. ```c OtioStatus otio_metadata_set_double( OtioDocument *target, OtioNode node_handle, const char *path, double value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `double` | input | | `out_error` | `text` | error | #### `set_int` `otio_metadata_set_int` · setter Writes a signed integer into the metadata. ```c OtioStatus otio_metadata_set_int( OtioDocument *target, OtioNode node_handle, const char *path, int64_t value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `int64` | input | | `out_error` | `text` | error | #### `set_null` `otio_metadata_set_null` · setter Writes a null into the metadata. ```c OtioStatus otio_metadata_set_null( OtioDocument *target, OtioNode node_handle, const char *path, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `out_error` | `text` | error | #### `set_object` `otio_metadata_set_object` · setter Writes a handle to an OTIO object into the metadata. The object stays where it is and the metadata names it. The handle must come from `target`, as every handle passed with a document must. Nothing checks that it is live, because a document being built may not hold the object yet. ```c OtioStatus otio_metadata_set_object( OtioDocument *target, OtioNode node_handle, const char *path, OtioNode value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `node` | input | | `out_error` | `text` | error | #### `set_rational_time` `otio_metadata_set_rational_time` · setter Writes a time into the metadata. ```c OtioStatus otio_metadata_set_rational_time( OtioDocument *target, OtioNode node_handle, const char *path, OtioRationalTime value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `OtioRationalTime` | input | | `out_error` | `text` | error | #### `set_string` `otio_metadata_set_string` · setter Writes a string into the metadata. ```c OtioStatus otio_metadata_set_string( OtioDocument *target, OtioNode node_handle, const char *path, const char *value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `text` | input | | `out_error` | `text` | error | #### `set_time_range` `otio_metadata_set_time_range` · setter Writes a span into the metadata. ```c OtioStatus otio_metadata_set_time_range( OtioDocument *target, OtioNode node_handle, const char *path, OtioTimeRange value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `OtioTimeRange` | input | | `out_error` | `text` | error | #### `set_time_transform` `otio_metadata_set_time_transform` · setter Writes a transform into the metadata. ```c OtioStatus otio_metadata_set_time_transform( OtioDocument *target, OtioNode node_handle, const char *path, OtioTimeTransform value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `OtioTimeTransform` | input | | `out_error` | `text` | error | #### `set_uint` `otio_metadata_set_uint` · setter Writes an unsigned integer into the metadata. ```c OtioStatus otio_metadata_set_uint( OtioDocument *target, OtioNode node_handle, const char *path, uint64_t value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `uint64` | input | | `out_error` | `text` | error | #### `set_v2d` `otio_metadata_set_v2d` · setter Writes a point into the metadata. ```c OtioStatus otio_metadata_set_v2d( OtioDocument *target, OtioNode node_handle, const char *path, OtioV2d value, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `value` | `OtioV2d` | input | | `out_error` | `text` | error | #### `set_vector` `otio_metadata_set_vector` · setter Writes an array of `len` nulls into the metadata, to be filled by index. ```c OtioStatus otio_metadata_set_vector( OtioDocument *target, OtioNode node_handle, const char *path, size_t len, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `path` | `text` | input | | `len` | `size` | input | | `out_error` | `text` | error | ### MissingReference > Media whose location is not known. Symbols in this group are spelled `otio_missing_reference_…`. An SDK drops that prefix. #### `new` `otio_missing_reference_new` · constructor Creates a media reference for media known to exist somewhere unknown. ```c OtioStatus otio_missing_reference_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | ### Node > What every object in a document can do. Symbols in this group are spelled `otio_node_…`. An SDK drops that prefix. #### `child_at` `otio_node_child_at` · method Returns one of an object's children. ```c OtioStatus otio_node_child_at( const OtioDocument *source, OtioNode parent, size_t index, OtioNode *out_child, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `index` | `size` | input | | `out_child` | `node` | output | | `out_error` | `text` | error | #### `child_count` `otio_node_child_count` · getter Returns how many children an object holds. Tracks, stacks, bare compositions and serializable collections hold children; anything else reports `OTIO_STATUS_WRONG_KIND`. ```c OtioStatus otio_node_child_count( const OtioDocument *source, OtioNode parent, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `out_count` | `size` | output | | `out_error` | `text` | error | #### `children` `otio_node_children` · getter Returns an object's children. ```c OtioStatus otio_node_children( const OtioDocument *source, OtioNode parent, OtioNode *out_nodes, size_t capacity, size_t *out_count, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `parent` | `node` | receiver | | `out_nodes` | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `equals` `otio_node_equal` · method Returns whether two handles have the same value. Within one document that is object identity: two handles are equal only if they were issued for the same occupant of the same slot. Handles from two different documents can be equal without naming the same object, since a handle is scoped to the document that issued it. ```c bool otio_node_equal(OtioNode left, OtioNode right); ``` | Parameter | Type | Role | | --- | --- | --- | | `left` | `node` | receiver | | `right` | `node` | input | #### `find_clips` `otio_node_find_clips` · getter Returns every clip at or below an object, in order. ```c * otio_node_find_clips(document, timeline, NULL, 0, &count, NULL); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_nodes` | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `highest_ancestor` `otio_node_highest_ancestor` · getter Returns the outermost object above this one. ```c OtioStatus otio_node_highest_ancestor( const OtioDocument *source, OtioNode node_handle, OtioNode *out_ancestor, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_ancestor` | `node` | output | | `out_error` | `text` | error | #### `is_none` `otio_node_is_none` · getter Returns whether a handle names no object. ```c bool otio_node_is_none(OtioNode node); ``` | Parameter | Type | Role | | --- | --- | --- | | `node` | `node` | receiver | #### `schema_kind` `otio_node_kind` · getter Returns what kind of object a handle names. The answer is the most specific kind the library knows: a clip reports `OTIO_NODE_KIND_CLIP`, never `OTIO_NODE_KIND_ITEM`, and a linear time warp `OTIO_NODE_KIND_LINEAR_TIME_WARP`, never `OTIO_NODE_KIND_TIME_EFFECT`. An object read from a schema the library does not know reports `OTIO_NODE_KIND_UNKNOWN_SCHEMA`. ```c OtioStatus otio_node_kind( const OtioDocument *source, OtioNode node_handle, OtioNodeKind *out_kind, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_kind` | `OtioNodeKind` | output | | `out_error` | `text` | error | #### `name` `otio_node_name` · getter Returns an object's name, which is empty for an object that has none. ```c OtioStatus otio_node_name( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_name` | `text` | output | | `out_error` | `text` | error | #### `none` `otio_node_none` · constructor Returns the handle that names no object. Fields that may be absent, such as a timeline's tracks before one is set, report this. Compare against it with `otio_node_is_none`. ```c * one, or `otio_node_none()` to leave the clip as it is. It returns * `OTIO_STATUS_OK`, or any other status to stop the read or write, having * written a NUL-terminated sentence to `message`, which has room for * `message_capacity` bytes, saying why. The call that ran it then fails with * `OTIO_STATUS_PLUGIN_ERROR` and that sentence. * * The registry is process-wide, so a plugin must be safe to call from any * thread that reads or writes. */ typedef OtioStatus (*OtioPluginFn)( void *context, OtioDocument *document, OtioNode target, OtioNode arguments, OtioNode *out_result, char *message, size_t message_capacity); ``` #### `overlapping` `otio_node_overlapping` · getter Returns whether an object sits over its neighbours rather than beside them. Only a transition does, which is why one does not advance the playhead when a track is laid out. ```c OtioStatus otio_node_overlapping( const OtioDocument *source, OtioNode node_handle, bool *out_overlapping, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_overlapping` | `bool` | output | | `out_error` | `text` | error | #### `parent` `otio_node_parent` · getter · may answer nothing Returns the composition an object sits in. Reports `OTIO_STATUS_NO_VALUE` for an object that is in none, which is what a freshly created one is. ```c OtioStatus otio_node_parent( const OtioDocument *source, OtioNode node_handle, OtioNode *out_parent, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_parent` | `node` | output | | `out_error` | `text` | error | #### `run_hook` `otio_node_run_hook` · method Runs every script attached to the hook `hook` on an object, each on what the one before returned, and writes what the last returned to `out_result`: upstream's `hooks.run`. `arguments` is the scripts' argument map as a JSON object, or null for an empty one. With no script attached, the object itself comes back. A hook that was never declared, by an attachment or as one of the four every read and write runs, is `OTIO_STATUS_PLUGIN_ERROR`, as is a script that fails or is not registered. ```c OtioStatus otio_node_run_hook( OtioDocument *document, OtioNode node_handle, const char *hook, const char *arguments, OtioNode *out_result, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `document` | `document` | document mut | | `node_handle` | `node` | receiver | | `hook` | `text` | input | | `arguments` (optional) | `text` | input | | `out_result` | `node` | output | | `out_error` | `text` | error | #### `schema_name` `otio_node_schema_name` · getter Returns the schema name an object serializes as, such as `"Clip"`. ```c OtioStatus otio_node_schema_name( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_name` | `text` | output | | `out_error` | `text` | error | #### `schema_version` `otio_node_schema_version` · getter Returns the schema version an object serializes as. ```c OtioStatus otio_node_schema_version( const OtioDocument *source, OtioNode node_handle, uint32_t *out_version, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_version` | `uint32` | output | | `out_error` | `text` | error | #### `set_name` `otio_node_set_name` · setter Sets an object's name. ```c OtioStatus otio_node_set_name( OtioDocument *target, OtioNode node_handle, const char *name, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `name` | `text` | input | | `out_error` | `text` | error | #### `to_json` `otio_node_to_json` · method Writes one object of a document as OTIO JSON. Upstream's `write_to_string` takes any object, not only a timeline, so this does too. ```c OtioStatus otio_node_to_json( const OtioDocument *source, OtioNode node, size_t indent, OtioBuffer *out_json, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node` | `node` | receiver | | `indent` | `size` | input | | `out_json` | `text` | output | | `out_error` | `text` | error | #### `transformed_time` `otio_node_transformed_time` · method Restates an instant from one object's clock in another's. ```c OtioStatus otio_node_transformed_time( const OtioDocument *source, OtioRationalTime time, OtioNode from, OtioNode to, OtioRationalTime *out_time, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `time` | `OtioRationalTime` | input | | `from` | `node` | receiver | | `to` | `node` | input | | `out_time` | `OtioRationalTime` | output | | `out_error` | `text` | error | #### `transformed_time_range` `otio_node_transformed_time_range` · method Restates a span from one object's clock in another's. ```c OtioStatus otio_node_transformed_time_range( const OtioDocument *source, OtioTimeRange range, OtioNode from, OtioNode to, OtioTimeRange *out_range, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `range` | `OtioTimeRange` | input | | `from` | `node` | receiver | | `to` | `node` | input | | `out_range` | `OtioTimeRange` | output | | `out_error` | `text` | error | #### `visible` `otio_node_visible` · getter Returns whether an object covers what is beneath it when its composition is flattened. ```c OtioStatus otio_node_visible( const OtioDocument *source, OtioNode node_handle, bool *out_visible, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_visible` | `bool` | output | | `out_error` | `text` | error | ### Ori > The ORI project's review annotations, an otio-rust extension and not part of OpenTimelineIO: strokes, text and shapes drawn on a frame, read and written on a timeline. Symbols in this group are spelled `otio_ext_ori_…`. An SDK drops that prefix. #### `add_commands` `otio_ext_ori_add_commands` · free Adds the commands in `commands_json`, a JSON list of ORI objects, at the end of the annotation clip `clip`'s commands. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `clip` | `node` | input | | `commands_json` | `text` | input | | `out_error` | `text` | error | #### `aspect_half` `otio_ext_ori_aspect_half` · free Writes half the width of an image `width` by `height` pixels in ORI's units, `width / (2 * height)`, to `out_half`: ORI's 16:9 default, 8/9, when the height is not positive. | Parameter | Type | Role | | --- | --- | --- | | `width` | `double` | input | | `height` | `double` | input | | `out_half` | `double` | output | | `out_error` | `text` | error | #### `bounds_to_ori` `otio_ext_ori_bounds_to_ori` · free Converts the point `x`, `y`, in OpenTimelineIO image space, to ORI's coordinates within a picture's `available_image_bounds`, the box from `min_x`, `min_y` to `max_x`, `max_y`, and writes them to `out_x` and `out_y`. The inverse of `otio_ext_ori_ori_to_bounds`. | Parameter | Type | Role | | --- | --- | --- | | `x` | `double` | input | | `y` | `double` | input | | `min_x` | `double` | input | | `min_y` | `double` | input | | `max_x` | `double` | input | | `max_y` | `double` | input | | `out_x` | `double` | output | | `out_y` | `double` | output | | `out_error` | `text` | error | #### `commands` `otio_ext_ori_commands` · free Writes the commands `holder` holds to `out_json`, as a JSON list of ORI objects, indented as OTIO files are with `pretty` and compact without. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `holder` | `node` | input | | `pretty` | `bool` | input | | `out_json` | `text` | output | | `out_error` | `text` | error | #### `commit` `otio_ext_ori_commit` · free Commits a review comment to the sync session `session`'s ledger, and writes its id to `out_id`: `client_mutation_id`, or a new one if that is empty. `anchor_json` is Furnace's anchor: `sequenceId`, `capturedGeneration`, `clipGuid`, `sourceTime` and `capturedSequenceTime` (each `{"value", "rate"}`), `capturedAt` in Unix milliseconds, and optionally `sessionId` (filled in when empty) and `mediaFingerprint`. `body_markdown` is the comment, and `commands_json` its strokes, text and shapes. `source` and `timeline` are this client's copy: the commit is refused, with `OTIO_STATUS_INVALID_ARGUMENT`, if it would replace commands that belong to someone else. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `source` (optional) | `document` | document in | | `timeline` | `node` | input | | `anchor_json` | `text` | input | | `body_markdown` | `text` | input | | `commands_json` | `text` | input | | `client_mutation_id` | `text` | input | | `out_id` | `text` | output | | `out_error` | `text` | error | #### `describe` `otio_ext_ori_describe` · free Writes what `holder`, an annotation clip or an `AnnotationEffect.1`, says about its annotation to `out_json`, as a JSON object: `form` (`track` or `effect`), `clip_guid` (the annotated clip's sync GUID, or `null`), `clip_name` (the clip's name where only that is known, as in ORI's review export, or `null`), `frame` (`{"value", "rate"}`, or `null` for an effect), `note` and `status` (text or `null`), and `command_count`. Reports `OTIO_STATUS_INVALID_ARGUMENT` for an object that holds no annotation. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `holder` | `node` | input | | `out_json` | `text` | output | | `out_error` | `text` | error | #### `enable` `otio_ext_ori_enable` · free Turns ORI Annotations on, and timeline sync which it is built on. The same as `otio_ext_enable("ori-annotations")`. Enabling it again does nothing and succeeds. | Parameter | Type | Role | | --- | --- | --- | | `out_error` | `text` | error | #### `ensure_clip` `otio_ext_ori_ensure_clip` · free Writes the annotation clip on `track` for `clip_guid` at the time `value` at `rate` to `out_clip`, first adding it if there is none: one frame long at that time, at the end of the track, after a gap if the time is later than the track's end, as ORI adds one. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `track` | `node` | input | | `clip_guid` | `text` | input | | `value` | `double` | input | | `rate` | `double` | input | | `out_clip` | `node` | output | | `out_error` | `text` | error | #### `ensure_track` `otio_ext_ori_ensure_track` · free Writes the annotation track of `timeline` to `out_track`, first adding a video track named `Annotations`, with a new sync GUID, on top of its stack if it has none. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `out_track` | `node` | output | | `out_error` | `text` | error | #### `find_clip` `otio_ext_ori_find_clip` · free · may answer nothing Finds the annotation clip on `track` for the media clip whose sync GUID is `clip_guid`, at source frame `frame`, and writes it to `out_clip`. Reports `OTIO_STATUS_NO_VALUE` when there is none. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `track` | `node` | input | | `clip_guid` | `text` | input | | `frame` | `int64` | input | | `out_clip` | `node` | output | | `out_error` | `text` | error | #### `find_track` `otio_ext_ori_find_track` · free · may answer nothing Finds the annotation track of `timeline`, the first track whose name contains "annotation" in any case, and writes it to `out_track`. Reports `OTIO_STATUS_NO_VALUE` when it has none. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `timeline` | `node` | input | | `out_track` | `node` | output | | `out_error` | `text` | error | #### `fold` `otio_ext_ori_fold` · free Folds the review comment a `review_relay` event, `event_json`, carries into `timeline`, on the host: adds the annotation track and clip if need be, merges the comment's commands in by uuid, and records the comment on the clip. Writes the clip to `out_clip`, and to `out_already` whether the comment was there already, so nothing changed. The host then publishes the timeline. Reports `OTIO_STATUS_INVALID_ARGUMENT` for another kind of event, or a comment that would replace commands that are not its author's, in which case nothing changes. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `event_json` | `text` | input | | `out_clip` | `node` | output | | `out_already` (optional) | `bool` | output | | `out_error` | `text` | error | #### `is_enabled` `otio_ext_ori_is_enabled` · free Writes whether ORI Annotations is on to `out_enabled`. | Parameter | Type | Role | | --- | --- | --- | | `out_enabled` | `bool` | output | | `out_error` | `text` | error | #### `list` `otio_ext_ori_list` · free Returns the object holding each annotation on `timeline`: an annotation clip, or an `AnnotationEffect.1` on a clip, in the order the timeline is walked. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `timeline` | `node` | input | | `out_nodes` | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `list_at_frame` `otio_ext_ori_list_at_frame` · free Returns the object holding each annotation of the media clip whose sync GUID is `clip_guid` at source frame `frame`. An `AnnotationEffect.1` covers every frame of its clip. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `timeline` | `node` | input | | `clip_guid` | `text` | input | | `frame` | `int64` | input | | `out_nodes` | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `list_for_clip` `otio_ext_ori_list_for_clip` · free Returns the object holding each annotation of the media clip whose sync GUID is `clip_guid`. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `timeline` | `node` | input | | `clip_guid` | `text` | input | | `out_nodes` | `list` | output list | | `capacity` | `size` | list capacity | | `out_count` | `size` | output count | | `out_error` | `text` | error | #### `merge_commands` `otio_ext_ori_merge_commands` · free Merges the commands in `commands_json` into the annotation clip `clip` by `uuid`: a stroke, text or shape the clip already holds is replaced where it was, and the rest are added at the end. Writes how many uuids it replaced to `out_replaced` and how many it added to `out_added`. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `clip` | `node` | input | | `commands_json` | `text` | input | | `out_replaced` | `size` | output | | `out_added` (optional) | `size` | output | | `out_error` | `text` | error | #### `merge_duplicates` `otio_ext_ori_merge_duplicates` · free Makes the annotation track of `timeline` hold one annotation clip for each media clip and source frame, merging later clips for the same frame into the first by uuid, and writes how many clips it removed to `out_count`. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `out_count` | `size` | output | | `out_error` | `text` | error | #### `new_uuid` `otio_ext_ori_new_uuid` · free Writes a new uuid for a command, a version 4 UUID, to `out_uuid`. | Parameter | Type | Role | | --- | --- | --- | | `out_uuid` | `text` | output | | `out_error` | `text` | error | #### `ori_to_bounds` `otio_ext_ori_ori_to_bounds` · free Converts the point `x`, `y`, in ORI's coordinates, to OpenTimelineIO image space within a picture's `available_image_bounds`, the box from `min_x`, `min_y` to `max_x`, `max_y`, whose centre is ORI's origin and whose height is ORI's unit, and writes it to `out_x` and `out_y`. | Parameter | Type | Role | | --- | --- | --- | | `x` | `double` | input | | `y` | `double` | input | | `min_x` | `double` | input | | `min_y` | `double` | input | | `max_x` | `double` | input | | `max_y` | `double` | input | | `out_x` | `double` | output | | `out_y` | `double` | output | | `out_error` | `text` | error | #### `ori_to_pixels` `otio_ext_ori_ori_to_pixels` · free Converts the point `x`, `y`, in ORI's coordinates, to a pixel position counted from the top left of an image `width` by `height` pixels, unrounded, and writes it to `out_px` and `out_py`. | Parameter | Type | Role | | --- | --- | --- | | `width` | `double` | input | | `height` | `double` | input | | `x` | `double` | input | | `y` | `double` | input | | `out_px` | `double` | output | | `out_py` | `double` | output | | `out_error` | `text` | error | #### `owner_of` `otio_ext_ori_owner_of` · free · may answer nothing Writes who the commands with `uuid` on the annotation clip `clip` belong to, by person id, to `out_owner`: the author of the review comment that added them. Reports `OTIO_STATUS_NO_VALUE` when no comment did, as for strokes drawn over ORI's wire or read from a file. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `clip` | `node` | input | | `uuid` | `text` | input | | `out_owner` | `text` | output | | `out_error` | `text` | error | #### `pixels_to_ori` `otio_ext_ori_pixels_to_ori` · free Converts the pixel position `px`, `py`, counted from the top left of an image `width` by `height` pixels, to ORI's height-normalised, centre-origin, Y-up coordinates, and writes them to `out_x` and `out_y`. | Parameter | Type | Role | | --- | --- | --- | | `width` | `double` | input | | `height` | `double` | input | | `px` | `double` | input | | `py` | `double` | input | | `out_x` | `double` | output | | `out_y` | `double` | output | | `out_error` | `text` | error | #### `read_partial` `otio_ext_ori_read_partial` · free · may answer nothing Writes the stroke in progress the sync event `event_json` carries to `out_partial_json`, as ORI's `PARTIAL` payload with every field a command left out filled in with ORI's default. Reports `OTIO_STATUS_NO_VALUE` when the event is not a stroke in progress. | Parameter | Type | Role | | --- | --- | --- | | `event_json` | `text` | input | | `out_partial_json` | `text` | output | | `out_error` | `text` | error | #### `remove_commands` `otio_ext_ori_remove_commands` · free Removes every command of `clip` whose `uuid` is in `uuids_json`, a JSON list of strings, and writes how many it removed to `out_count`. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `clip` | `node` | input | | `uuids_json` | `text` | input | | `out_count` | `size` | output | | `out_error` | `text` | error | #### `replace_by_kind` `otio_ext_ori_replace_by_kind` · free Replaces the commands of `clip` kind by kind with those in `commands_json`: an empty list, `[]`, clears it, but a list holding no command of one kind (strokes, text, each kind of shape) leaves the clip's commands of that kind alone. Writes how many commands it removed to `out_removed`. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `clip` | `node` | input | | `commands_json` | `text` | input | | `out_removed` | `size` | output | | `out_error` | `text` | error | #### `replace_commands` `otio_ext_ori_replace_commands` · free Replaces every command of `clip` whose `uuid` is `uuid`, such as one whole stroke, with the commands in `commands_json`, put where the first was, and writes whether any had that uuid to `out_replaced`. If none did, nothing changes. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `clip` | `node` | input | | `uuid` | `text` | input | | `commands_json` | `text` | input | | `out_replaced` | `bool` | output | | `out_error` | `text` | error | #### `send_partial` `otio_ext_ori_send_partial` · free Sends a stroke in progress to everyone in the sync session `session`: `partial_json` is ORI's `PARTIAL` payload, an object with `clip_guid`, `frame`, `fps` and `events`, the commands drawn so far. Peers show it and never store it. A newer one not yet sent replaces it. Furnace cannot relay it yet, and refuses it with `OTIO_STATUS_UNSUPPORTED` when it is about to be sent. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `partial_json` | `text` | input | | `out_error` | `text` | error | #### `set_commands` `otio_ext_ori_set_commands` · free Replaces all of `clip`'s commands with those in `commands_json`, as ORI's `REPLACE_ANNOTATION_COMMANDS` does. An empty list, `[]`, clears it. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `clip` | `node` | input | | `commands_json` | `text` | input | | `out_error` | `text` | error | #### `timestamp_now` `otio_ext_ori_timestamp_now` · free Writes the current time in ISO 8601, for a command's `timestamp`, to `out_timestamp`. | Parameter | Type | Role | | --- | --- | --- | | `out_timestamp` | `text` | output | | `out_error` | `text` | error | ### Plugins > Media linkers and hook scripts: upstream's two plugin points, registered as functions. Symbols in this group are spelled `otio_attach_…` or `otio_detach_…` or `otio_register_…` or `otio_unregister_…`. An SDK drops that prefix. #### `attach_hook_script` `otio_attach_hook_script` · free Attaches the hook script `script` to the hook `hook`, after any attached already, declaring the hook if it is new. The four hooks every read and write runs are `post_adapter_read`, `post_media_linker`, `pre_adapter_write` and `post_adapter_write`. Any other name declares a hook of the caller's own, which [`otio_node_run_hook`] runs. The script need not be registered yet, but must be by the time the hook runs. ```c OtioStatus otio_attach_hook_script( const char *hook, const char *script, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `hook` | `text` | input | | `script` | `text` | input | | `out_error` | `text` | error | #### `detach_hook_script` `otio_detach_hook_script` · free Detaches every attachment of the hook script `script` from the hook `hook`. Returns whether it was attached. ```c bool otio_detach_hook_script(const char *hook, const char *script); ``` | Parameter | Type | Role | | --- | --- | --- | | `hook` | `text` | input | | `script` | `text` | input | #### `register_hook_script` `otio_register_hook_script` · free Registers a hook script under `name`, replacing any registered already. It runs only at the hooks it is attached to, with [`otio_attach_hook_script`]. `context` is handed to `function` on every call, and to `release`, which may be null, once the script is replaced or unregistered. A call that fails registers nothing and releases nothing. ```c OtioStatus otio_register_hook_script( const char *name, OtioPluginFn function, void *context, OtioPluginReleaseFn release, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `name` | `text` | input | | `function` | `plugin` | input | | `context` (optional) | `context` | plugin context | | `release` (optional) | `release` | plugin release | | `out_error` | `text` | error | #### `register_media_linker` `otio_register_media_linker` · free Registers a media linker under `name`, replacing any registered already. A read uses it when its options name it, or when the `OTIO_DEFAULT_MEDIA_LINKER` environment variable does and the options name none. `context` is handed to `function` on every call, and to `release`, which may be null, once the linker is replaced or unregistered. A call that fails registers nothing and releases nothing. ```c OtioStatus otio_register_media_linker( const char *name, OtioPluginFn function, void *context, OtioPluginReleaseFn release, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `name` | `text` | input | | `function` | `plugin` | input | | `context` (optional) | `context` | plugin context | | `release` (optional) | `release` | plugin release | | `out_error` | `text` | error | #### `unregister_hook_script` `otio_unregister_hook_script` · free Unregisters the hook script registered under `name`, releasing its context. Returns whether there was one. A hook it is still attached to fails when it runs, as upstream's does for a script its manifest lists and cannot find; detach it as well. ```c bool otio_unregister_hook_script(const char *name); ``` | Parameter | Type | Role | | --- | --- | --- | | `name` | `text` | input | #### `unregister_media_linker` `otio_unregister_media_linker` · free Unregisters the media linker registered under `name`, releasing its context. Returns whether there was one. ```c bool otio_unregister_media_linker(const char *name); ``` | Parameter | Type | Role | | --- | --- | --- | | `name` | `text` | input | ### Rate > What the SMPTE timecode rates are, and which of them drop frames. Symbols in this group are spelled `otio_is_…` or `otio_nearest_…`. An SDK drops that prefix. #### `is_drop_frame_rate` `otio_is_drop_frame_rate` · free Returns whether a rate is a drop-frame rate. ```c bool otio_is_drop_frame_rate(double rate); ``` | Parameter | Type | Role | | --- | --- | --- | | `rate` | `double` | input | #### `is_smpte_timecode_rate` `otio_is_smpte_timecode_rate` · free Returns whether a rate is one SMPTE timecode is defined for. ```c bool otio_is_smpte_timecode_rate(double rate); ``` | Parameter | Type | Role | | --- | --- | --- | | `rate` | `double` | input | #### `nearest_smpte_timecode_rate` `otio_nearest_smpte_timecode_rate` · free Returns the SMPTE timecode rate closest to a rate. ```c double otio_nearest_smpte_timecode_rate(double rate); ``` | Parameter | Type | Role | | --- | --- | --- | | `rate` | `double` | input | ### RationalTime > A point in time, or a length of it, as a value over a rate. Symbols in this group are spelled `otio_rational_time_…`. An SDK drops that prefix. #### `add` `otio_rational_time_add` · method Returns the sum of two times, at the higher of the two rates. ```c OtioRationalTime otio_rational_time_add(OtioRationalTime left, OtioRationalTime right); ``` | Parameter | Type | Role | | --- | --- | --- | | `left` | `OtioRationalTime` | receiver | | `right` | `OtioRationalTime` | input | #### `almost_equals` `otio_rational_time_almost_equal` · method Returns whether two times are within `delta` of each other. ```c bool otio_rational_time_almost_equal( OtioRationalTime left, OtioRationalTime right, double delta); ``` | Parameter | Type | Role | | --- | --- | --- | | `left` | `OtioRationalTime` | receiver | | `right` | `OtioRationalTime` | input | | `delta` | `double` | input | #### `ceil` `otio_rational_time_ceil` · getter Returns the time rounded towards positive infinity. ```c OtioRationalTime otio_rational_time_ceil(OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | #### `compare` `otio_rational_time_compare` · method Orders two times: -1 if `left` is earlier, 1 if later, 0 if the same. A comparison involving a NaN has no answer, and reports 0. ```c int32_t otio_rational_time_compare(OtioRationalTime left, OtioRationalTime right); ``` | Parameter | Type | Role | | --- | --- | --- | | `left` | `OtioRationalTime` | receiver | | `right` | `OtioRationalTime` | input | #### `duration_from_start_end_time` `otio_rational_time_duration_from_start_end_time` · free Returns how long it is from one instant to another, the end excluded. ```c OtioRationalTime otio_rational_time_duration_from_start_end_time( OtioRationalTime start_time, OtioRationalTime end_time_exclusive); ``` | Parameter | Type | Role | | --- | --- | --- | | `start_time` | `OtioRationalTime` | input | | `end_time_exclusive` | `OtioRationalTime` | input | #### `duration_from_start_end_time_inclusive` `otio_rational_time_duration_from_start_end_time_inclusive` · free Returns how long it is from one instant to another, the end included. ```c OtioRationalTime otio_rational_time_duration_from_start_end_time_inclusive( OtioRationalTime start_time, OtioRationalTime end_time_inclusive); ``` | Parameter | Type | Role | | --- | --- | --- | | `start_time` | `OtioRationalTime` | input | | `end_time_inclusive` | `OtioRationalTime` | input | #### `equals` `otio_rational_time_equal` · method Returns whether two times are the same instant. Times at different rates that name the same instant compare equal; this is upstream's `==`, not a field-by-field comparison. For that, use [`otio_rational_time_strictly_equal`]. ```c bool otio_rational_time_equal(OtioRationalTime left, OtioRationalTime right); ``` | Parameter | Type | Role | | --- | --- | --- | | `left` | `OtioRationalTime` | receiver | | `right` | `OtioRationalTime` | input | #### `floor` `otio_rational_time_floor` · getter Returns the time rounded towards negative infinity. ```c OtioRationalTime otio_rational_time_floor(OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | #### `from_frames` `otio_rational_time_from_frames` · constructor Builds a time from a frame number at a rate. ```c OtioRationalTime otio_rational_time_from_frames(double frame, double rate); ``` | Parameter | Type | Role | | --- | --- | --- | | `frame` | `double` | input | | `rate` | `double` | input | #### `from_seconds` `otio_rational_time_from_seconds` · constructor Builds a time from a number of seconds, at a rate of one. ```c OtioRationalTime otio_rational_time_from_seconds(double seconds); ``` | Parameter | Type | Role | | --- | --- | --- | | `seconds` | `double` | input | #### `from_seconds_at_rate` `otio_rational_time_from_seconds_at_rate` · constructor Builds a time from a number of seconds at a rate. ```c OtioRationalTime otio_rational_time_from_seconds_at_rate(double seconds, double rate); ``` | Parameter | Type | Role | | --- | --- | --- | | `seconds` | `double` | input | | `rate` | `double` | input | #### `from_time_string` `otio_rational_time_from_time_string` · constructor Reads a time from a `[-]HH:MM:SS.sss` time string at a rate. ```c OtioStatus otio_rational_time_from_time_string( const char *time_string, double rate, OtioRationalTime *out_time, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `time_string` | `text` | input | | `rate` | `double` | input | | `out_time` | `OtioRationalTime` | output | | `out_error` | `text` | error | #### `from_timecode` `otio_rational_time_from_timecode` · constructor Reads a time from a `HH:MM:SS:FF` timecode at a rate. ```c OtioStatus otio_rational_time_from_timecode( const char *timecode, double rate, OtioRationalTime *out_time, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `timecode` | `text` | input | | `rate` | `double` | input | | `out_time` | `OtioRationalTime` | output | | `out_error` | `text` | error | #### `is_valid` `otio_rational_time_is_valid` · getter Returns whether a time is usable: both parts finite and the rate positive. ```c bool otio_rational_time_is_valid(OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | #### `negate` `otio_rational_time_negate` · getter Returns the time with its sign flipped. ```c OtioRationalTime otio_rational_time_negate(OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | #### `rescaled_to` `otio_rational_time_rescaled_to` · method Returns the same instant expressed at another rate. ```c OtioRationalTime otio_rational_time_rescaled_to(OtioRationalTime time, double rate); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | | `rate` | `double` | input | #### `rescaled_to_time` `otio_rational_time_rescaled_to_time` · method Returns the same instant expressed at another time's rate. ```c OtioRationalTime otio_rational_time_rescaled_to_time( OtioRationalTime time, OtioRationalTime other); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | | `other` | `OtioRationalTime` | input | #### `round` `otio_rational_time_round` · getter Returns the time rounded to the nearest whole value, halves away from zero. ```c OtioRationalTime otio_rational_time_round(OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | #### `strictly_equals` `otio_rational_time_strictly_equal` · method Returns whether two times have the same value and the same rate. ```c bool otio_rational_time_strictly_equal(OtioRationalTime left, OtioRationalTime right); ``` | Parameter | Type | Role | | --- | --- | --- | | `left` | `OtioRationalTime` | receiver | | `right` | `OtioRationalTime` | input | #### `subtract` `otio_rational_time_subtract` · method Returns the difference of two times, at the higher of the two rates. ```c OtioRationalTime otio_rational_time_subtract( OtioRationalTime left, OtioRationalTime right); ``` | Parameter | Type | Role | | --- | --- | --- | | `left` | `OtioRationalTime` | receiver | | `right` | `OtioRationalTime` | input | #### `to_frames` `otio_rational_time_to_frames` · getter Returns the frame number this time falls on. ```c int32_t otio_rational_time_to_frames(OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | #### `to_frames_at_rate` `otio_rational_time_to_frames_at_rate` · method Returns the frame number this time falls on at another rate. ```c int32_t otio_rational_time_to_frames_at_rate(OtioRationalTime time, double rate); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | | `rate` | `double` | input | #### `to_nearest_timecode_at` `otio_rational_time_to_nearest_timecode_at` · method Writes a time as a timecode, rounding to the nearest frame first. ```c OtioStatus otio_rational_time_to_nearest_timecode_at( OtioRationalTime time, double rate, OtioDropFrame drop_frame, OtioBuffer *out_timecode, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | | `rate` | `double` | input | | `drop_frame` | `OtioDropFrame` | input | | `out_timecode` | `text` | output | | `out_error` | `text` | error | #### `to_seconds` `otio_rational_time_to_seconds` · getter Returns the time in seconds. ```c double otio_rational_time_to_seconds(OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | #### `to_time_string` `otio_rational_time_to_time_string` · getter Writes a time as a `HH:MM:SS.sss` time string. ```c OtioStatus otio_rational_time_to_time_string( OtioRationalTime time, OtioBuffer *out_string, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | | `out_string` | `text` | output | | `out_error` | `text` | error | #### `to_timecode` `otio_rational_time_to_timecode` · getter Writes a time as a timecode at its own rate. ```c OtioStatus otio_rational_time_to_timecode( OtioRationalTime time, OtioBuffer *out_timecode, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | | `out_timecode` | `text` | output | | `out_error` | `text` | error | #### `to_timecode_at` `otio_rational_time_to_timecode_at` · method Writes a time as a timecode at a given rate and drop-frame setting. ```c OtioStatus otio_rational_time_to_timecode_at( OtioRationalTime time, double rate, OtioDropFrame drop_frame, OtioBuffer *out_timecode, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | | `rate` | `double` | input | | `drop_frame` | `OtioDropFrame` | input | | `out_timecode` | `text` | output | | `out_error` | `text` | error | #### `value_rescaled_to` `otio_rational_time_value_rescaled_to` · method Returns what this time's value would be at another rate. ```c double otio_rational_time_value_rescaled_to(OtioRationalTime time, double rate); ``` | Parameter | Type | Role | | --- | --- | --- | | `time` | `OtioRationalTime` | receiver | | `rate` | `double` | input | ### SerializableCollection > A bag of objects, held together for writing to a file. Symbols in this group are spelled `otio_serializable_collection_…`. An SDK drops that prefix. #### `new` `otio_serializable_collection_new` · constructor Creates a serializable collection: a group of objects with no timing. ```c OtioStatus otio_serializable_collection_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | ### Stack > A composition that starts all its children together. Symbols in this group are spelled `otio_stack_…`. An SDK drops that prefix. #### `new` `otio_stack_new` · constructor Creates a stack. ```c OtioStatus otio_stack_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | ### Sync > Timeline sync, an otio-rust extension and not part of OpenTimelineIO: sharing a timeline between programs while it is edited. Symbols in this group are spelled `otio_ext_sync_…`. An SDK drops that prefix. #### `apply` `otio_ext_sync_apply` · free Applies the JSON list of patches `patches` to the timeline JSON `timeline`, as Furnace's `otiodiff` does, and writes the patched timeline to `out_timeline` as compact JSON. | Parameter | Type | Role | | --- | --- | --- | | `timeline` | `text` | input | | `patches` | `text` | input | | `out_timeline` | `text` | output | | `out_error` | `text` | error | #### `apply_to` `otio_ext_sync_apply_to` · free Applies the JSON list of patches `patches` to `timeline` in place. The timeline keeps its handle, as does every synced object in it that the patches keep; one they remove is stale afterwards. On failure the document is left as it was. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `patches` | `text` | input | | `out_error` | `text` | error | #### `begin` `otio_ext_sync_begin` · free Starts a transaction on `timeline`, which is `sequence_id` at `generation`: stamps any missing GUIDs, and writes the timeline as it is to `out_before` as a snapshot. The snapshot is the transaction. Keep it, edit the timeline, and pass it to `otio_ext_sync_commit` for the patches, or to `otio_ext_sync_rollback` to undo the edits. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `sequence_id` | `text` | input | | `generation` | `int64` | input | | `out_before` | `text` | output | | `out_error` | `text` | error | #### `commit` `otio_ext_sync_commit` · free Ends the transaction `otio_ext_sync_begin` wrote as `before`, writing the patches that take the timeline as it was to the timeline as it is, the next generation, to `out_patches` as a compact JSON list. An edit that changed nothing writes `[]`. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `before` | `text` | input | | `out_patches` | `text` | output | | `out_error` | `text` | error | #### `commit_journal` `otio_ext_sync_commit_journal` · free Takes what the journal of the document `timeline` is in recorded since it started or was last committed, runs the `document_changed` hook on the document's root with it if anything changed, and writes whether anything did to `out_changed`. The hook's arguments are `touched`, the GUIDs of the synced objects that were added or may have changed, and `root_changed`. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `out_changed` | `bool` | output | | `out_error` | `text` | error | #### `diff` `otio_ext_sync_diff` · free Works out the patches that take the snapshot `old_snapshot` to the snapshot `new_snapshot`, as Furnace's `otiodiff` does, and writes them to `out_patches` as a JSON list. A snapshot is JSON as Furnace sends one: `sequenceId`, `generation`, and the timeline as `otioJson`. With `pretty`, the list is indented as Furnace's golden files are; otherwise it is compact. A change too large for patches, or one between sequences or back in generation, is one `replace_timeline` patch; `otio_ext_sync_diff_stats` says why. | Parameter | Type | Role | | --- | --- | --- | | `old_snapshot` | `text` | input | | `new_snapshot` | `text` | input | | `pretty` | `bool` | input | | `out_patches` | `text` | output | | `out_error` | `text` | error | #### `diff_stats` `otio_ext_sync_diff_stats` · free Writes what `otio_ext_sync_diff` does with the same snapshots to `out_stats`, as a JSON object: `insertOps`, `moveOps`, `removeOps`, `setPropertyOps`, `totalOps`, `nodeCount`, `fellBack` and `fallbackReason`. | Parameter | Type | Role | | --- | --- | --- | | `old_snapshot` | `text` | input | | `new_snapshot` | `text` | input | | `out_stats` | `text` | output | | `out_error` | `text` | error | #### `enable` `otio_ext_sync_enable` · free Turns timeline sync on. The same as `otio_ext_enable("sync")`. Enabling it again does nothing and succeeds. | Parameter | Type | Role | | --- | --- | --- | | `out_error` | `text` | error | #### `find` `otio_ext_sync_find` · free · may answer nothing Finds the object under `timeline` that carries the GUID `guid`, and writes it to `out_node`. Reports `OTIO_STATUS_NO_VALUE` when none does, and `OTIO_STATUS_INVALID_ARGUMENT` when two do; stamp first to rule that out. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `timeline` | `node` | input | | `guid` | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `guid` `otio_ext_sync_guid` · free · may answer nothing Writes the GUID `node` carries in `metadata.sync.guid` to `out_guid`. Reports `OTIO_STATUS_NO_VALUE` for an object that carries none. | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node` | `node` | input | | `out_guid` | `text` | output | | `out_error` | `text` | error | #### `handle` `otio_ext_sync_handle` · free Acts on the JSON `event` from `otio_ext_sync_session_poll_event` in `timeline`, the program's copy of the timeline the event names, and writes what it did to `out_handled` as a JSON object: `handled`, one of `applied`, `replaced`, `rejected`, `joined` or `ignored`, and for `rejected` the script's `message`. A `timeline` event runs `sync_before_apply`, whose scripts may refuse it, applies its patches so the timeline and the objects that survive keep their handles, falling back to the whole snapshot if the patches do not fit (`replaced`), runs the default media linker on what it inserted, then `sync_after_apply`. A `joined` event runs `sync_joined`. Every other event is the program's to act on, and is `ignored`. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `event` | `text` | input | | `out_handled` | `text` | output | | `out_error` | `text` | error | #### `is_enabled` `otio_ext_sync_is_enabled` · free Writes whether timeline sync is on to `out_enabled`. | Parameter | Type | Role | | --- | --- | --- | | `out_enabled` | `bool` | output | | `out_error` | `text` | error | #### `new_guid` `otio_ext_sync_new_guid` · free Writes a new GUID, a version 4 UUID, to `out_guid`. | Parameter | Type | Role | | --- | --- | --- | | `out_guid` | `text` | output | | `out_error` | `text` | error | #### `rollback` `otio_ext_sync_rollback` · free Ends the transaction `otio_ext_sync_begin` wrote as `before`, putting the timeline back as it was. The timeline and every synced object that was in it keep their handles. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `before` | `text` | input | | `out_error` | `text` | error | #### `session_cancel_review` `otio_ext_sync_session_cancel_review` · free Stops sending the review commit `client_mutation_id`, and writes whether it was waiting to `out_cancelled`. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `client_mutation_id` | `text` | input | | `out_cancelled` | `bool` | output | | `out_error` | `text` | error | #### `session_close` `otio_ext_sync_session_close` · free Releases `session`, whose number names nothing afterwards. Closing one already closed does nothing and succeeds. A session another call is using is released when that call returns. It sends nothing: call `otio_ext_sync_session_leave` and send the goodbye first to leave politely. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `out_error` | `text` | error | #### `session_commit_review` `otio_ext_sync_session_commit_review` · free Commits a review comment, the JSON object `commit`, to the session's ledger, and writes its id to `out_id`. `commit` holds `anchor`, `bodyMarkdown`, `annotationCommands` and `clientMutationId`, which is made up if left out. The commit is sent again after every reconnect until the server acknowledges or refuses it. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `commit` | `text` | input | | `out_id` | `text` | output | | `out_error` | `text` | error | #### `session_connect` `otio_ext_sync_session_connect` · free Starts joining on a new connection: call it once the socket is open, and again after a reconnect. The hello is the next frame to send. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `out_error` | `text` | error | #### `session_disconnected` `otio_ext_sync_session_disconnected` · free Tells `session` the socket closed. It waits for `otio_ext_sync_session_connect`, keeping its timelines and the review commits not yet acknowledged. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `out_error` | `text` | error | #### `session_leave` `otio_ext_sync_session_leave` · free Leaves the session: the goodbye is the next frame to send, after which the socket can close. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `out_error` | `text` | error | #### `session_may_send` `otio_ext_sync_session_may_send` · free Writes whether `session` may send on `channel` now to `out_may`: it has joined, and its role holds the channel's lease. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `channel` | `text` | input | | `out_may` | `bool` | output | | `out_error` | `text` | error | #### `session_new` `otio_ext_sync_session_new` · free Makes a session from the JSON object `settings`, and writes the number that names it to `out_session`. Release it with `otio_ext_sync_session_close`. `settings` holds `token`, the token the server issued; `identity`, who the client is (`displayName`, `app`, `appVersion`, `credential`, `deviceId`, `clientKind`, `formFactor`, `deviceName` and `capabilities`); and `encodings`, the compression it accepts (`br`, `gzip`). Each may be left out: `{}` is a guest desktop editor accepting every encoding. | Parameter | Type | Role | | --- | --- | --- | | `settings` | `text` | input | | `out_session` | `uint64` | output | | `out_error` | `text` | error | #### `session_poll_event` `otio_ext_sync_session_poll_event` · free · may answer nothing Writes the next event to `out_event` as a JSON object, whose `type` is `joined`, `timeline`, `playhead`, `selection`, `roster`, `leader`, `review_relay`, `review_committed`, `review_rejected`, `error`, `ended` or `message`. Reports `OTIO_STATUS_NO_VALUE` when there is none. Pass a `timeline` or `joined` event to `otio_ext_sync_handle` to apply it. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `out_event` | `text` | output | | `out_error` | `text` | error | #### `session_poll_transmit` `otio_ext_sync_session_poll_transmit` · free · may answer nothing Writes the next frame to send to `out_bytes`, stamped with `now` in Unix milliseconds, and whether to send it as a binary WebSocket frame rather than a text one to `out_binary`. Reports `OTIO_STATUS_NO_VALUE` when there is nothing to send. Call it until then after every other session call. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `now` | `int64` | input | | `out_bytes` | `bytes` | output | | `out_binary` | `bool` | output | | `out_error` | `text` | error | #### `session_publish` `otio_ext_sync_session_publish` · free Sends `timeline`, as the sequence `sequence_id`, to the session, and writes the patches from the version sent before to `out_patches` as a JSON list. Only the host may. Stamps missing GUIDs and runs `sync_before_send`, whose scripts may stop it. Sending a timeline that has not changed sends nothing and writes `[]`, unless it switches the active sequence. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `target` | `document` | document mut | | `timeline` | `node` | input | | `sequence_id` | `text` | input | | `out_patches` | `text` | output | | `out_error` | `text` | error | #### `session_receive` `otio_ext_sync_session_receive` · free Hands `session` a frame that arrived: `binary` says whether it was a binary WebSocket frame rather than a text one. A failure means the connection broke the protocol or lost frames: close the socket, call `otio_ext_sync_session_disconnected`, and connect again. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `data` | `bytes` | bytes | | `len` | `size` | length | | `binary` | `bool` | input | | `out_error` | `text` | error | #### `session_register_channel` `otio_ext_sync_session_register_channel` · free Registers the channel `name`, for another extension's messages, with the delivery rule `delivery`: `latest_wins`, `ordered` or `transient`. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `name` | `text` | input | | `delivery` | `text` | input | | `out_error` | `text` | error | #### `session_send` `otio_ext_sync_session_send` · free Sends a message of type `kind` with the JSON `payload` on `channel`. It is for what sync has no call of its own for: on Furnace, `signal` on the signal channel, and the host's `bin`, `lan_mode_set` and `lan_decision` on the control channel. Furnace closes a connection that sends a type it does not know, so anything else is refused with `OTIO_STATUS_UNSUPPORTED` when it is about to be sent. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `channel` | `text` | input | | `kind` | `text` | input | | `payload` | `text` | input | | `out_error` | `text` | error | #### `session_set_leader` `otio_ext_sync_session_set_leader` · free Makes `participant` the playback leader. Only the host may. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `participant` | `text` | input | | `out_error` | `text` | error | #### `session_set_muted` `otio_ext_sync_session_set_muted` · free Mutes or unmutes `participant`. Only the host may. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `participant` | `text` | input | | `muted` | `bool` | input | | `out_error` | `text` | error | #### `session_set_playhead` `otio_ext_sync_session_set_playhead` · free Moves this client's playhead in `sequence_id` to `frame`, playing at `rate` or stopped. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `sequence_id` | `text` | input | | `frame` | `double` | input | | `playing` | `bool` | input | | `rate` | `double` | input | | `out_error` | `text` | error | #### `session_set_selection` `otio_ext_sync_session_set_selection` · free Selects the objects in `sequence_id` whose GUIDs the JSON list `guids` holds. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `sequence_id` | `text` | input | | `guids` | `text` | input | | `out_error` | `text` | error | #### `session_state` `otio_ext_sync_session_state` · free Writes where `session` stands to `out_state` as a JSON object: `joined`, `ended`, `sessionId`, `selfId`, `selfUserId`, `role`, `leader`, `peers`, `activeSequence`, `sequences`, `pendingReviews`, and the protocol `version` and `encodings` the server chose. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `out_state` | `text` | output | | `out_error` | `text` | error | #### `session_timeline` `otio_ext_sync_session_timeline` · free · may answer nothing Writes the latest snapshot of `sequence_id` that `session` sent or received to `out_snapshot`. Reports `OTIO_STATUS_NO_VALUE` when it has none. | Parameter | Type | Role | | --- | --- | --- | | `session` | `uint64` | input | | `sequence_id` | `text` | input | | `out_snapshot` | `text` | output | | `out_error` | `text` | error | #### `set_guid` `otio_ext_sync_set_guid` · free Gives `node` the GUID `guid`, replacing any it had. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node` | `node` | input | | `guid` | `text` | input | | `out_error` | `text` | error | #### `stamp` `otio_ext_sync_stamp` · free Gives every object sync addresses under `timeline` a GUID, if it lacks one or shares one with an object met earlier, and writes how many it gave one to to `out_count`. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `out_count` | `size` | output | | `out_error` | `text` | error | #### `start_journal` `otio_ext_sync_start_journal` · free Starts recording which objects any edit to the document `timeline` is in touches. The journal is the document's, so it records edits anywhere in it; the timeline only says which document. Recording again does nothing and succeeds. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `out_error` | `text` | error | #### `stop_journal` `otio_ext_sync_stop_journal` · free Stops recording edits to the document `timeline` is in, and forgets what was recorded since the last commit. | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `timeline` | `node` | input | | `out_error` | `text` | error | ### TimeEffect > An effect that alters timing. Symbols in this group are spelled `otio_time_effect_…`. An SDK drops that prefix. #### `new` `otio_time_effect_new` · constructor Creates a time effect: an effect that alters timing and has no parameters. ```c OtioStatus otio_time_effect_new( OtioDocument *target, const char *name, const char *effect_name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `effect_name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | ### TimeRange > A span of time: where it starts and how long it lasts. Symbols in this group are spelled `otio_time_range_…`. An SDK drops that prefix. #### `before_range` `otio_time_range_before_range` · method Returns whether this range ends before another begins. ```c bool otio_time_range_before_range( OtioTimeRange range, OtioTimeRange other, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | | `epsilon_s` | `double` | input | #### `before_time` `otio_time_range_before_time` · method Returns whether this range ends before an instant. ```c bool otio_time_range_before_time( OtioTimeRange range, OtioRationalTime time, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `time` | `OtioRationalTime` | input | | `epsilon_s` | `double` | input | #### `begins_range` `otio_time_range_begins_range` · method Returns whether another range starts where this one does. ```c bool otio_time_range_begins_range( OtioTimeRange range, OtioTimeRange other, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | | `epsilon_s` | `double` | input | #### `begins_time` `otio_time_range_begins_time` · method Returns whether this range starts at an instant. ```c bool otio_time_range_begins_time( OtioTimeRange range, OtioRationalTime time, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `time` | `OtioRationalTime` | input | | `epsilon_s` | `double` | input | #### `clamped_range` `otio_time_range_clamped_range` · method Returns the range pulled inside this one, where it lies outside it. ```c OtioTimeRange otio_time_range_clamped_range(OtioTimeRange range, OtioTimeRange other); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | #### `clamped_time` `otio_time_range_clamped_time` · method Returns the instant pulled inside the range, if it lies outside it. ```c OtioRationalTime otio_time_range_clamped_time( OtioTimeRange range, OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `time` | `OtioRationalTime` | input | #### `contains_range` `otio_time_range_contains_range` · method Returns whether another range falls entirely inside this one. ```c bool otio_time_range_contains_range( OtioTimeRange range, OtioTimeRange other, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | | `epsilon_s` | `double` | input | #### `contains_time` `otio_time_range_contains_time` · method Returns whether an instant falls inside the range, the end excluded. ```c bool otio_time_range_contains_time(OtioTimeRange range, OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `time` | `OtioRationalTime` | input | #### `duration_extended_by` `otio_time_range_duration_extended_by` · method Returns the range with its duration lengthened. ```c OtioTimeRange otio_time_range_duration_extended_by( OtioTimeRange range, OtioRationalTime by); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `by` | `OtioRationalTime` | input | #### `end_time_exclusive` `otio_time_range_end_time_exclusive` · getter Returns the instant just after the range's end. ```c OtioRationalTime otio_time_range_end_time_exclusive(OtioTimeRange range); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | #### `end_time_inclusive` `otio_time_range_end_time_inclusive` · getter Returns the last instant the range covers. ```c OtioRationalTime otio_time_range_end_time_inclusive(OtioTimeRange range); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | #### `extended_by` `otio_time_range_extended_by` · method Returns the smallest range covering both of two ranges. ```c OtioTimeRange otio_time_range_extended_by(OtioTimeRange range, OtioTimeRange other); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | #### `finishes_range` `otio_time_range_finishes_range` · method Returns whether another range ends where this one does. ```c bool otio_time_range_finishes_range( OtioTimeRange range, OtioTimeRange other, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | | `epsilon_s` | `double` | input | #### `finishes_time` `otio_time_range_finishes_time` · method Returns whether this range ends at an instant. ```c bool otio_time_range_finishes_time( OtioTimeRange range, OtioRationalTime time, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `time` | `OtioRationalTime` | input | | `epsilon_s` | `double` | input | #### `from_start_end_time` `otio_time_range_from_start_end_time` · constructor Builds a range from its start and the instant after its end. ```c OtioTimeRange otio_time_range_from_start_end_time( OtioRationalTime start_time, OtioRationalTime end_time_exclusive); ``` | Parameter | Type | Role | | --- | --- | --- | | `start_time` | `OtioRationalTime` | input | | `end_time_exclusive` | `OtioRationalTime` | input | #### `from_start_end_time_inclusive` `otio_time_range_from_start_end_time_inclusive` · constructor Builds a range from its start and its last instant. ```c OtioTimeRange otio_time_range_from_start_end_time_inclusive( OtioRationalTime start_time, OtioRationalTime end_time_inclusive); ``` | Parameter | Type | Role | | --- | --- | --- | | `start_time` | `OtioRationalTime` | input | | `end_time_inclusive` | `OtioRationalTime` | input | #### `intersects` `otio_time_range_intersects` · method Returns whether two ranges share more than a single boundary instant. ```c bool otio_time_range_intersects( OtioTimeRange range, OtioTimeRange other, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | | `epsilon_s` | `double` | input | #### `is_valid` `otio_time_range_is_valid` · getter Returns whether a range is usable: valid times and a duration of at least zero. ```c bool otio_time_range_is_valid(OtioTimeRange range); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | #### `meets` `otio_time_range_meets` · method Returns whether this range ends exactly where another begins. ```c bool otio_time_range_meets(OtioTimeRange range, OtioTimeRange other, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | | `epsilon_s` | `double` | input | #### `overlaps_range` `otio_time_range_overlaps_range` · method Returns whether two ranges share any time at all. ```c bool otio_time_range_overlaps_range( OtioTimeRange range, OtioTimeRange other, double epsilon_s); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `other` | `OtioTimeRange` | input | | `epsilon_s` | `double` | input | #### `overlaps_time` `otio_time_range_overlaps_time` · method Returns whether an instant falls inside the range, the ends included. ```c bool otio_time_range_overlaps_time(OtioTimeRange range, OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `range` | `OtioTimeRange` | receiver | | `time` | `OtioRationalTime` | input | ### TimeTransform > An offset and a scale, applied to a time or a range. Symbols in this group are spelled `otio_time_transform_…`. An SDK drops that prefix. #### `applied_to_range` `otio_time_transform_applied_to_range` · method Returns the transform applied to a span. ```c OtioTimeRange otio_time_transform_applied_to_range( OtioTimeTransform transform, OtioTimeRange range); ``` | Parameter | Type | Role | | --- | --- | --- | | `transform` | `OtioTimeTransform` | receiver | | `range` | `OtioTimeRange` | input | #### `applied_to_time` `otio_time_transform_applied_to_time` · method Returns the transform applied to an instant. ```c OtioRationalTime otio_time_transform_applied_to_time( OtioTimeTransform transform, OtioRationalTime time); ``` | Parameter | Type | Role | | --- | --- | --- | | `transform` | `OtioTimeTransform` | receiver | | `time` | `OtioRationalTime` | input | #### `applied_to_transform` `otio_time_transform_applied_to_transform` · method Returns the transform applied to another transform. ```c OtioTimeTransform otio_time_transform_applied_to_transform( OtioTimeTransform transform, OtioTimeTransform other); ``` | Parameter | Type | Role | | --- | --- | --- | | `transform` | `OtioTimeTransform` | receiver | | `other` | `OtioTimeTransform` | input | ### Timeline > A whole edit: a stack of tracks and where its clock starts. Symbols in this group are spelled `otio_timeline_…`. An SDK drops that prefix. #### `clear_global_start_time` `otio_timeline_clear_global_start_time` · clearer Clears where a timeline begins. ```c OtioStatus otio_timeline_clear_global_start_time( OtioDocument *target, OtioNode node_handle, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `out_error` | `text` | error | #### `global_start_time` `otio_timeline_global_start_time` · getter · may answer nothing Returns where a timeline begins, such as `01:00:00:00`. Reports `OTIO_STATUS_NO_VALUE` for a timeline that does not say. ```c OtioStatus otio_timeline_global_start_time( const OtioDocument *source, OtioNode node_handle, OtioRationalTime *out_time, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_time` | `OtioRationalTime` | output | | `out_error` | `text` | error | #### `new` `otio_timeline_new` · constructor Creates a timeline, with an empty stack named `"tracks"` already in it. Upstream's `Timeline()` builds that stack in its constructor, and its own tests append to a fresh timeline's tracks without making one first, so a timeline from here arrives the same way rather than leaving every binding to invent the difference. Replace it with [`otio_timeline_set_tracks`] to use a stack of your own; the one built here is thrown away with the document. ```c OtioStatus otio_timeline_new( OtioDocument *target, const char *name, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `set_global_start_time` `otio_timeline_set_global_start_time` · setter Sets where a timeline begins. ```c OtioStatus otio_timeline_set_global_start_time( OtioDocument *target, OtioNode node_handle, OtioRationalTime time, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `time` | `OtioRationalTime` | input | | `out_error` | `text` | error | #### `set_tracks` `otio_timeline_set_tracks` · setter Sets the stack holding a timeline's tracks. The stack's parent is set to the timeline, as upstream's does. Passing `otio_node_none` puts a fresh empty stack there rather than nothing, because that is what upstream's setter does: its own `test_timeline.py` sets `tracks` to `None` and then asserts that `tl.tracks` is still a `Stack`. A timeline with no tracks at all is not a thing a caller can reach through upstream's API, so it is not one they can reach through this one. Whatever stack was there is not destroyed. It stays in the document, parentless, so it can be put somewhere else; dropping it is a separate `otio_document_remove` call. That is the same bargain `otio_composition_detach_child` makes, and leaving its parent pointing at the timeline instead would mean an object claiming a parent that has disowned it. A displaced stack that some other timeline has since taken as its own keeps that timeline as its parent, since this one is not the timeline disowning it. ```c OtioStatus otio_timeline_set_tracks( OtioDocument *target, OtioNode node_handle, OtioNode tracks, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `tracks` (optional) | `node` | input | | `out_error` | `text` | error | #### `tracks` `otio_timeline_tracks` · getter · may answer nothing Returns the stack holding a timeline's tracks. Reports `OTIO_STATUS_NO_VALUE` for a timeline that has none. ```c OtioStatus otio_timeline_tracks( const OtioDocument *source, OtioNode node_handle, OtioNode *out_tracks, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_tracks` | `node` | output | | `out_error` | `text` | error | ### Track > A composition that lays its children end to end. Symbols in this group are spelled `otio_track_…`. An SDK drops that prefix. #### `kind` `otio_track_kind` · getter Returns what a track carries, such as `"Video"` or `"Audio"`. ```c OtioStatus otio_track_kind( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_kind, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_kind` | `text` | output | | `out_error` | `text` | error | #### `new` `otio_track_new` · constructor Creates a track. `kind` may be null, which means `"Video"`, as upstream's default does. ```c OtioStatus otio_track_new( OtioDocument *target, const char *name, const char *kind, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `kind` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `set_kind` `otio_track_set_kind` · setter Sets what a track carries. ```c OtioStatus otio_track_set_kind( OtioDocument *target, OtioNode node_handle, const char *kind, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `kind` | `text` | input | | `out_error` | `text` | error | ### Transition > A dissolve or a wipe. Symbols in this group are spelled `otio_transition_…`. An SDK drops that prefix. #### `enabled` `otio_transition_enabled` · getter Returns whether a transition is applied. ```c OtioStatus otio_transition_enabled( const OtioDocument *source, OtioNode node_handle, bool *out_enabled, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_enabled` | `bool` | output | | `out_error` | `text` | error | #### `in_offset` `otio_transition_in_offset` · getter Returns how far a transition reaches into the item before it. ```c OtioStatus otio_transition_in_offset( const OtioDocument *source, OtioNode node_handle, OtioRationalTime *out_offset, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_offset` | `OtioRationalTime` | output | | `out_error` | `text` | error | #### `new` `otio_transition_new` · constructor Creates a transition. Its offsets start at zero. ```c OtioStatus otio_transition_new( OtioDocument *target, const char *name, const char *transition_type, OtioNode *out_node, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `name` (optional) | `text` | input | | `transition_type` (optional) | `text` | input | | `out_node` | `node` | output | | `out_error` | `text` | error | #### `out_offset` `otio_transition_out_offset` · getter Returns how far a transition reaches into the item after it. ```c OtioStatus otio_transition_out_offset( const OtioDocument *source, OtioNode node_handle, OtioRationalTime *out_offset, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_offset` | `OtioRationalTime` | output | | `out_error` | `text` | error | #### `set_enabled` `otio_transition_set_enabled` · setter Sets whether a transition is applied. ```c OtioStatus otio_transition_set_enabled( OtioDocument *target, OtioNode node_handle, bool enabled, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `enabled` | `bool` | input | | `out_error` | `text` | error | #### `set_in_offset` `otio_transition_set_in_offset` · setter Sets how far a transition reaches into the item before it. ```c OtioStatus otio_transition_set_in_offset( OtioDocument *target, OtioNode node_handle, OtioRationalTime offset, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `offset` | `OtioRationalTime` | input | | `out_error` | `text` | error | #### `set_out_offset` `otio_transition_set_out_offset` · setter Sets how far a transition reaches into the item after it. ```c OtioStatus otio_transition_set_out_offset( OtioDocument *target, OtioNode node_handle, OtioRationalTime offset, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `offset` | `OtioRationalTime` | input | | `out_error` | `text` | error | #### `set_type` `otio_transition_set_type` · setter Sets the kind of transition. ```c OtioStatus otio_transition_set_type( OtioDocument *target, OtioNode node_handle, const char *transition_type, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `target` | `document` | document mut | | `node_handle` | `node` | receiver | | `transition_type` | `text` | input | | `out_error` | `text` | error | #### `type` `otio_transition_type` · getter Returns the kind of transition, such as `"SMPTE_Dissolve"`. ```c OtioStatus otio_transition_type( const OtioDocument *source, OtioNode node_handle, OtioBuffer *out_type, OtioBuffer *out_error); ``` | Parameter | Type | Role | | --- | --- | --- | | `source` (optional) | `document` | document in | | `node_handle` | `node` | receiver | | `out_type` | `text` | output | | `out_error` | `text` | error |