# Swift

> A SwiftPM package over libotio, with a class per schema, real enums and throwing calls.

The Swift package links `libotio`, the C ABI built from the Rust core, and is
generated from that ABI, so it carries the whole data model: the schemas, the
composition algorithms, the ten edit operations and the file-format adapters.
Its shape is OpenTimelineIO's own Swift bindings: a class per schema deriving
as the schemas derive, an initializer per schema, values as structs, real
enums, `throws` for failure, and no document in the surface.

```swift
import OpenTimelineIO
```

## Install

The package links against a static `libotio` it expects to find in
`sdk/swift/lib/`, and the linker is told where that is on the command line.
The library is not checked in, so build it from the Rust core first:

```sh
cargo build -p otio-capi --release
cp target/release/libotio.a sdk/swift/lib/

cd sdk/swift
swift test -Xlinker -L"$PWD/lib"
```

Each [GitHub release](https://github.com/alchemist-editor/otio-rust/releases)
also carries the Swift SDK's sources, laid out as in the repository with an
empty `lib/`, and a prebuilt `libotio-<version>-<target>.tar.gz` per target
whose `lib/` holds the library to put there.

## Your first program

```swift
import OpenTimelineIO

// An EDL never says what rate its timecode is at, so this has to be right: a
// file read at the wrong rate puts every event in the wrong place rather
// than failing.
let options = ReadOptions(rate: 24, nameColumn: "", ignoreTimecodeMismatch: false)

// Reading hands back what the file is about, not a container to look inside.
let root = try OTIO.readFromFile(.cmx3600, path: "cut.edl", options: options)

for case let clip as Clip in try root.findClips() {
    print(try clip.name())
}
```

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

Reading hands back what the file is about, its root object, rather than a
container to look inside. `OTIO.open` infers the format from the filename
instead of taking one, and `OTIO.save` writes back the same way. The rate is
the one option an EDL cannot do without; see [CMX 3600 EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/).

`for case let clip as Clip` works because every object arrives as the class of
its schema, so `as?` asks what one really is:

```swift
for child in try track.children() {
    if let clip = child as? Clip, let reference = try clip.mediaReference() as? ExternalReference {
        print(try reference.targetURL())
    }
}
```

## Building a timeline

```swift
import OpenTimelineIO

// Each object is made on its own and joins a timeline when you put it into
// one. Nothing has to exist before the thing it goes into.
let timeline = try Timeline(name: "Cut")
let stack = try Stack(name: "tracks")
let track = try Track(name: "V1", kind: "Video")

try timeline.setTracks(stack)
try stack.appendChild(track)

for (index, name) in ["A", "B", "C"].enumerated() {
    let clip = try Clip(name: name)
    try clip.setSourceRange(
        TimeRange(
            startTime: RationalTime(value: Double(index * 24), rate: 24),
            duration: RationalTime(value: 24, rate: 24)))
    try track.appendChild(clip)
}

// Three seconds of picture, written as canonical OpenTimelineIO JSON.
// Objects keep their timeline alive between them, so there is nothing to
// close.
print(try track.duration().toSeconds)
try OTIO.save(timeline, to: "cut.otio")
```

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

Each object is made on its own and joins a timeline when you put it in one.
Values such as `RationalTime` and `TimeRange` are structs.

## How objects and documents work here

There is no document to hold. Underneath, the core keeps a timeline's objects
in an arena; the package does that bookkeeping, and objects keep their
timeline alive between them, so there is nothing to close. `close()` exists
for releasing a large one early, and every object that lived in it then fails
with `.nullPointer` rather than reading freed memory.

Objects made apart stay apart until one takes the other in. Appending or
inserting an object that is still a child in another timeline is refused as
the library refuses it, with `.coreError` and the library's own message, and
so is placing one whose handle has gone stale, with `.staleHandle`. Both are
refused before that timeline is brought over, so both stay whole, and closing
one leaves the other working.

Compositions are deliberately not Swift collections; children are read with
`children()`.

## Errors and missing values

A call that can fail throws an `OTIOError` carrying a `Status`.

A call that only *names* an object, such as `detachChild`, `indexOfChild` or
`hasChild`, refuses one that belongs to a different timeline, and refuses it
before asking the library, because merging the two and failing afterwards
would already have done the damage. That refusal is an `OTIOError` with
`.invalidArgument` and `isOtherTimeline` set; the other timeline is untouched.

Where "there is nothing here" is one of the answers, such as an item with no
source range or a clip with no active media reference, the call answers `nil`,
because that is an answer rather than a failure:

```swift
if let span = try clip.sourceRange() {
    print(span)
}
```

## Editing and algorithms

```swift
import OpenTimelineIO

/// One second of picture, named.
func second(_ name: String) throws -> Clip {
    let clip = try Clip(name: name)
    try clip.setSourceRange(
        TimeRange(
            startTime: RationalTime(value: 0, rate: 24),
            duration: RationalTime(value: 24, rate: 24)))
    return clip
}

func show(_ track: Track) throws {
    let names = try track.children().map { try $0.name() }.joined(separator: " ")
    print(names, "-", try track.duration().value, "frames")
}

let track = try Track(name: "V1", kind: "Video")
for name in ["A", "B", "C"] {
    try track.appendChild(try second(name))
}
try show(track)

// Insert makes room: everything from the insertion point onwards moves later,
// and the track gets longer.
try OTIO.insert(
    try second("D"), composition: track,
    time: RationalTime(value: 24, rate: 24), removeTransitions: false)
try 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.
try OTIO.overwrite(
    try second("E"), composition: track,
    range: TimeRange(
        startTime: RationalTime(value: 48, rate: 24),
        duration: RationalTime(value: 24, rate: 24)),
    removeTransitions: false)
try show(track)
```

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

The ten edit operations and the algorithms are static functions on `OTIO`,
such as `OTIO.insert`, `OTIO.overwrite` and `OTIO.flattenTracks`, because each
is about two objects and belongs to neither.
[Editing a timeline](https://otio-rust-docs.vercel.app/docs/guides/editing/) says what each edit does.

## Formats

`OTIO.readFromFile` and `OTIO.writeToFile` take a format, such as `.cmx3600`
or `.fcpxXML`. Every format the C ABI has is here: OTIO JSON, ALE, CMX 3600
EDL, both FCP XML flavours, AAF, and the `.otioz` and `.otiod` bundles, which
are read and written through a path only.

```swift
import OpenTimelineIO

// An EDL never says what rate its timecode is at, so this has to be right: a
// file read at the wrong rate puts every event in the wrong place rather
// than failing.
let options = ReadOptions(rate: 24, nameColumn: "", ignoreTimecodeMismatch: false)
let root = try OTIO.readFromFile(.cmx3600, path: "cut.edl", options: options)

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

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

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/).

## Media linkers and hooks

```swift
import OpenTimelineIO

// A cut of two clips, as an .otio file would hold it.
let 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. nil leaves
// the clip as it was.
try OTIO.registerMediaLinker("proxies") { clip, arguments in
    let name = try clip.name()
    let root = try arguments.getString("root")
    return try ExternalReference(name: name, targetURL: "\(root)/\(name).mov")
}

// A hook script is handed the whole result, and answers with what the read
// goes on with: here the same object, stamped.
try OTIO.registerHookScript("stamp") { target, arguments in
    let who = try arguments.getString("who")
    if let named = target as? SerializableObjectWithMetadata {
        try named.metadata.setString("read_by", value: who)
    }
    return target
}
try OTIO.attachHookScript("post_adapter_read", script: "stamp")

// The read names the linker, and carries both sets of arguments as JSON.
let track = try OTIO.readFromBytes(
    .otioJSON, data: Array(cut.utf8),
    options: ReadOptions(
        mediaLinker: "proxies",
        mediaLinkerArguments: #"{"root": "/proxies"}"#,
        hookArguments: #"{"who": "the conform"}"#))

if let named = track as? SerializableObjectWithMetadata {
    let who = try named.metadata.getString("read_by")
    print("read by", who)
}
for case let clip as Clip in try track.findClips() {
    if let media = try clip.mediaReference() as? ExternalReference {
        let name = try clip.name()
        let url = try media.targetURL()
        print(name, "->", url)
    }
}

// The registry is the whole process's, so a program that is done with its
// plugins says so.
_ = OTIO.detachHookScript("post_adapter_read", script: "stamp")
_ = OTIO.unregisterHookScript("stamp")
_ = OTIO.unregisterMediaLinker("proxies")
```

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

A media linker and a hook script are closures, registered under a name with
`OTIO.registerMediaLinker` and `OTIO.registerHookScript`. A linker is a
`MediaLinker`, handed a `Clip` and the arguments as `Metadata`, and answers a
`MediaReference?`, where `nil` leaves the clip alone. A hook script is a
`HookScript`, handed what the hook runs on and answering what to go on with.
`ReadOptions` names the linker and carries both sets of arguments as JSON;
`attachHookScript` puts a script at a hook, and `runHook` runs a hook of your
own on an object.

What a closure throws, of any error type, fails the read, write or `runHook`
with `.pluginError` and the error's description; it is caught where the
library called in and never crosses into it. What a closure is handed is lent
for the call only and fails if kept. While it runs, the timeline it was handed
cannot be closed or moved into another one, since the call that lent it is
still using it. The library keeps the closure until the name is registered
again or unregistered, and may call it from whichever thread reads.

## Platforms

CI builds and tests the package on Linux and macOS. Windows is left out: the
package links the Rust static library through the platform's toolchain, which
CI does not have set up for MSVC.

## Reference

- [`sdk/swift/README.md`](https://github.com/alchemist-editor/otio-rust/blob/main/sdk/swift/README.md).
- The [C ABI reference](https://otio-rust-docs.vercel.app/reference/) the package is generated from, and
  [ADR 0003](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0003-sdk-generation.md)
  for every deliberate departure from upstream's Swift bindings.

---

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