# TypeScript

> The core compiled to WebAssembly and published to npm, for the browser and for Node, with no document to hold.

The TypeScript package, [`@alchemist-edit/otio`](https://www.npmjs.com/package/@alchemist-edit/otio),
is the C ABI compiled to `wasm32-unknown-unknown` with a TypeScript object
model on top. The module has no imports, so one package serves both the
browser and Node with nothing native to install. The TypeScript is written by
the same generator as the other SDKs, from the same description of the C ABI,
and the class hierarchy and names are upstream OpenTimelineIO's, spelled the
way TypeScript spells things: `sourceRange` where Python says `source_range`.

## Install

```sh
npm install @alchemist-edit/otio
```

The package declares Node 20 or newer. Call `init()` once before anything
else; calling it again does nothing. In Node it reads the module off disk. In
a browser it fetches it from beside the JavaScript with
`new URL("otio_wasm.wasm", import.meta.url)`, which Vite, webpack, Rollup and
esbuild all understand.

When the module is served from somewhere else, such as a CDN, pass its URL:
`init(url)`. When you already have the bytes or a compiled module, hand them
to `initAsync` or `initSync` instead. The module itself is exported as
`@alchemist-edit/otio/wasm` for bundlers that want to be told where it is.

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        # cargo build --target wasm32-unknown-unknown, then tsc
npm test             # the suite, in Node
npm run test:browser # the same suite, in Chromium
```

Versions are published from an `npm-v<version>` tag with npm's trusted
publishing, so each version on npm carries a provenance statement that
`npm audit signatures` checks. A pre-release such as `0.2.0-rc.1` goes to the
`next` dist-tag, so a plain `npm install` never picks one up.

## Your first program

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

await init();

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

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

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

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

There is no file system behind a WebAssembly module, so the adapters take and
return strings or bytes, and getting hold of them is the host's line of code:
`fs` in Node, `fetch` in a browser. `readTimelineFromString` is
`readFromString` with a check that the file held a timeline, which most files
do and most callers want. The rate is the one option an EDL cannot do
without; see [CMX 3600 EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/).

## Building a timeline

```typescript
import { writeFile } from "node:fs/promises";
import { init, Clip, RationalTime, Timeline, TimeRange, Track, serializeJsonToString } from "@alchemist-edit/otio";

await init();

// A fresh timeline arrives with an empty stack called `tracks`, as
// upstream's does, so there is nothing to build before appending to it.
const timeline = new Timeline({ name: "Cut" });
const track = new Track({ name: "V1", kind: "Video" });
timeline.tracks?.appendChild(track);

["A", "B", "C"].forEach((name, index) => {
  const clip = new Clip({ name });
  clip.sourceRange = new TimeRange(
    new RationalTime(index * 24, 24),
    new RationalTime(24, 24),
  );
  track.appendChild(clip);
});

// Three seconds of picture, written as canonical OpenTimelineIO JSON.
console.log(track.duration().toSeconds());
await writeFile("cut.otio", serializeJsonToString(timeline));
```

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

A new `Timeline` arrives with an empty stack called `tracks`, as upstream's
does. Constructors take an object of named fields, and stored fields such as
`name` and `sourceRange` are properties.

## How objects and documents work here

The core keeps every object in a document's arena and names it with a handle.
Upstream's API has no document in it, and neither does this one:

- each object built on its own gets a document of its own;
- putting one inside another moves it there, and the emptied document keeps a
  note saying where its contents went, so a wrapper handed out before the move
  goes on working;
- reading the same object twice gives the same wrapper, so `===` means what a
  JavaScript programmer expects;
- a document is released by a `FinalizationRegistry`, which is late but
  correct, or by `dispose()` for code that would rather say when. Everything
  that lived in it throws afterwards rather than reading memory that has been
  handed back.

An object that is still a child in another timeline is refused when it is
appended or inserted, and one whose handle has gone stale is refused wherever
it is placed, both before anything moves, so both timelines stay whole.

## Errors and missing values

A call the library fails throws an `OtioError`, whose `status` says which kind
of failure it was: `"staleHandle"` for an object used after it was removed,
`"parseError"` for a file that was not what it claimed to be, `"wrongKind"`
for a call made on the wrong kind of object or metadata value, `"coreError"`
for a refusal from the core, and the rest of the C ABI's statuses spelled the
same way.

A call that only names an object, such as `hasChild`, `detachChild` or
`flattenTracks`, refuses one from another timeline before the library is
asked, with an `OtherTimelineError`. It carries no status and is not an
`OtioError`, so `err instanceof OtherTimelineError` is how to recognise it.

"There is nothing here" is `undefined`: the `sourceRange` of an untrimmed
item, the `parent()` of an object that is in nothing.

One failure is particular to WebAssembly. `wasm32-unknown-unknown` has no
unwinding, so a panic in the Rust core traps instead of being caught at the
boundary. The module is then marked poisoned and every later call throws an
`OtioPanic`; load a fresh one with `init()`.

## Editing and algorithms

```typescript
import { init, Clip, edit, RationalTime, TimeRange, Track } from "@alchemist-edit/otio";

await init();

/** One second of picture, named. */
function second(name: string): Clip {
  const clip = new Clip({ name });
  clip.sourceRange = new TimeRange(
    new RationalTime(0, 24),
    new RationalTime(24, 24),
  );
  return clip;
}

function show(track: Track) {
  const names = track.children().map((child) => child.name).join(" ");
  console.log(names, "-", track.duration().value, "frames");
}

const track = new Track({ name: "V1", kind: "Video" });
for (const name of ["A", "B", "C"]) track.appendChild(second(name));
show(track);

// Insert makes room: everything from the insertion point onwards moves later,
// and the track gets longer.
edit.insert(second("D"), track, new RationalTime(24, 24), false);
show(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.
edit.overwrite(
  second("E"),
  track,
  new TimeRange(new RationalTime(48, 24), new RationalTime(24, 24)),
  false,
);
show(track);
```

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

The ten edit operations are functions on the exported `edit` object, such as
`edit.insert`, `edit.overwrite` and `edit.fill`, and the algorithms are on
`algorithms`: `flattenStack`, `flattenTracks` and `trackTrimmedToRange`.
[Editing a timeline](https://otio-rust-docs.vercel.app/docs/guides/editing/) says what each edit does.

## Formats

`readFromString`, `readFromBytes`, `writeToString` and `writeToBytes` take a
format by name: `"otioJson"`, `"ale"`, `"cmx3600"`, `"fcp7Xml"`, `"fcpxXml"`
or `"aaf"`. `serializeJsonToString` and `deserializeJsonFromString` are
upstream's names for `.otio` JSON, and start wherever they are pointed, so a
bare clip serialises as happily as a whole timeline.

EDL, ALE, both FCP XML flavours and AAF are Python-only plugins upstream, so a
WebAssembly build of upstream's C++ core cannot read them. Here they are Rust
and compile to `wasm32` unchanged. AAF is the one that wanted something from
the host: a written AAF records when it was made and gives itself random
identifiers, and the module has neither a clock nor randomness, so
`writeToBytes` passes the time and a fresh seed on every write.

```typescript
import { writeFile } from "node:fs/promises";
import {
  init,
  Clip,
  ExternalReference,
  RationalTime,
  Timeline,
  TimeRange,
  Track,
  readFromBytes,
  writeToBytes,
} from "@alchemist-edit/otio";

await init();

const timeline = new Timeline({ name: "Cut" });
const track = new Track({ name: "V1", kind: "Video" });
timeline.tracks?.appendChild(track);

const oneSecond = new TimeRange(new RationalTime(0, 24), new RationalTime(24, 24));

// 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.
for (const name of ["A001C003", "A001C004"]) {
  const media = new ExternalReference({ targetUrl: `file:///media/${name}.mov` });
  media.availableRange = oneSecond;

  const clip = new Clip({ name });
  clip.setMediaReference("DEFAULT_MEDIA", media);
  clip.activeMediaReferenceKey = "DEFAULT_MEDIA";
  clip.sourceRange = oneSecond;
  track.appendChild(clip);
}

// 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. The module has no clock or
// randomness of its own, so this side supplies the time and a seed.
const aaf = writeToBytes("aaf", timeline, { aafUseEmptyMobIds: true });
await writeFile("cut.aaf", aaf);

for (const clip of readFromBytes("aaf", aaf).findClips()) {
  console.log(clip.name);
}
```

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

The bundle formats, `.otioz` and `.otiod`, are not available here. A bundle is
a directory, or an archive of media copied in from files on disk, and the
module has no file system to find either on. Rather than offer a format that
can only fail, the package leaves them out of its surface: write the timeline
as `.otio` and bundle it where there is a file system. See
[Bundles](https://otio-rust-docs.vercel.app/docs/formats/bundles/) and
[Reading and writing files](https://otio-rust-docs.vercel.app/docs/guides/reading-and-writing/).

## Media linkers and hooks

```typescript
import {
  init,
  attachHookScript,
  detachHookScript,
  ExternalReference,
  readFromString,
  registerHookScript,
  registerMediaLinker,
  unregisterHookScript,
  unregisterMediaLinker,
} from "@alchemist-edit/otio";

await init();

// A cut of two clips, as an .otio file would hold it.
const 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, with the arguments the
// read was given, and answers with the media the clip should use. Answering
// nothing leaves the clip as it was; throwing stops the read.
registerMediaLinker("proxies", (clip, args) =>
  new ExternalReference({ name: clip.name, targetUrl: `${args.root}/${clip.name}.mov` }),
);

// A hook script is handed the whole result, and answers with what the read
// goes on with: here the same object, stamped.
registerHookScript("stamp", (target, args) => {
  target.metadata.set("read_by", String(args.who));
  return target;
});
attachHookScript("post_adapter_read", "stamp");

try {
  // The read names the linker, and carries both sets of arguments as JSON.
  const track = readFromString("otioJson", cut, {
    mediaLinker: "proxies",
    mediaLinkerArguments: JSON.stringify({ root: "/proxies" }),
    hookArguments: JSON.stringify({ who: "the conform" }),
  });

  console.log("read by", track.metadata.get("read_by"));
  for (const clip of track.findClips()) {
    const media = clip.mediaReference();
    if (media instanceof ExternalReference) {
      console.log(clip.name, "->", media.targetUrl);
    }
  }
} finally {
  detachHookScript("post_adapter_read", "stamp");
  unregisterHookScript("stamp");
  unregisterMediaLinker("proxies");
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/link-media-and-run-hooks/typescript.ts</sub>

`registerMediaLinker(name, (clip, args) => ...)` registers a media linker, as
upstream's `link_media_reference`: it answers the media reference the clip
should use, or nothing to leave the clip alone. A read runs it when its
options' `mediaLinker` names it, unless `doNotLinkMedia` is set.
`registerHookScript(name, (target, args) => ...)` registers a hook script, as
upstream's `hook_function`: it answers the object to go on with, the one it
was handed or another. `attachHookScript` puts it at a hook: the four every
read and write runs, or one of your own that `node.runHook(hook, args)` runs.
`args` is the JSON object the read, write or `runHook` was given, as a plain
object.

A plugin's failure is a thrown error. It stops the read or write, which throws
an `OtioError` with status `"pluginError"` and the plugin's own message. A
plugin runs inside the call that needs it, so it cannot be `async`.

What a plugin is handed belongs to the read, write or hook that called it, and
throws once the plugin returns, so keep values out of it rather than the
objects themselves. What it answers may be built fresh, with `new
ExternalReference(...)` and the like, and joins that timeline. Moving what it
was handed into another timeline, or disposing of the timeline a hook is
running on, is refused.

A WebAssembly module cannot be handed a JavaScript function, so the module
imports one dispatcher from the package instead, and each registration is a
number the package looks the function up by. The registry belongs to the
module, so a fresh `init` after an `OtioPanic` starts with none registered.

## Platforms

Browsers and Node. CI runs the same test suite in both, in Node and in
Chromium, and `npm run check:pack` installs the packed tarball into an empty
project and runs it there before a release.

## Reference

- [`crates/otio-wasm/README.md`](https://github.com/alchemist-editor/otio-rust/blob/main/crates/otio-wasm/README.md):
  how the module and the package are made, and why there is no wasm-bindgen.
- [`crates/otio-wasm/ts/README.md`](https://github.com/alchemist-editor/otio-rust/blob/main/crates/otio-wasm/ts/README.md):
  the README npm shows.
- The [C ABI reference](https://otio-rust-docs.vercel.app/reference/) the package is generated from.

---

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