# Go

> A cgo package over libotio, with methods, embedded structs for the schema ladder, and an error for every failure.

The Go package is cgo over `libotio`, the C ABI built from the Rust core. It
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. What things are called follows upstream OpenTimelineIO's own
bindings; what the package *is* follows Go, with methods, slices, strings and
an `error` rather than out-parameters and status codes.

```go
import otio "github.com/alchemist-editor/otio-rust/sdk/go"
```

## Install

The package links against a static `libotio` it expects to find in
`sdk/go/lib/`. The library is not checked in, so build it from the Rust core
and copy it there:

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

cd sdk/go
go test ./...
```

Each [GitHub release](https://github.com/alchemist-editor/otio-rust/releases)
also carries the Go 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

```go
package main

import (
	"fmt"
	"log"

	otio "github.com/alchemist-editor/otio-rust/sdk/go"
)

func main() {
	// An EDL never says what rate its timecode is at, so this has to be
	// right: a file read at the wrong rate puts every event in the wrong
	// place rather than failing.
	//
	// Reading answers with what the file was about — the root object — and
	// there is no container to hold on to besides it.
	root, err := otio.ReadFromFile(otio.FormatCMX3600, "cut.edl", &otio.ReadOptions{Rate: 24})
	if err != nil {
		log.Fatal(err)
	}
	defer root.Close()

	clips, err := root.FindClips()
	if err != nil {
		log.Fatal(err)
	}
	for _, clip := range otio.Filter(clips, otio.Node.AsClip) {
		name, _ := clip.Name()
		fmt.Println(name)
	}
}
```

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

Reading answers with what the file was about, the root object, and there is no
container around it to hold. `otio.Open` infers the format from the filename
instead of taking one, and `otio.Save` writes back the same way. The rate in
`ReadOptions` is the one thing an EDL cannot tell you; see
[CMX 3600 EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/).

`otio.Filter` narrows a list of nodes with one of the `As` methods, here
keeping only the clips.

## Building a timeline

```go
package main

import (
	"fmt"
	"log"

	otio "github.com/alchemist-editor/otio-rust/sdk/go"
)

func main() {
	// An object is built on its own and put together with the others
	// afterwards. Nothing has to exist before the thing it goes into.
	timeline, err := otio.NewTimeline("Cut")
	if err != nil {
		log.Fatal(err)
	}
	// A timeline is released when it is collected, so this is not required.
	// It is worth doing anyway: it frees the whole thing at once, at a
	// moment you chose.
	defer timeline.Close()

	stack, err := otio.NewStack("tracks")
	if err != nil {
		log.Fatal(err)
	}
	track, err := otio.NewTrack("V1", "Video")
	if err != nil {
		log.Fatal(err)
	}
	if err := timeline.SetTracks(&stack.Node); err != nil {
		log.Fatal(err)
	}
	if err := stack.AppendChild(track.Node); err != nil {
		log.Fatal(err)
	}

	for index, name := range []string{"A", "B", "C"} {
		clip, err := otio.NewClip(name)
		if err != nil {
			log.Fatal(err)
		}
		span := otio.TimeRange{
			StartTime: otio.RationalTime{Value: float64(index) * 24, Rate: 24},
			Duration:  otio.RationalTime{Value: 24, Rate: 24},
		}
		if err := clip.SetSourceRange(span); err != nil {
			log.Fatal(err)
		}
		// Appending moves the clip into the timeline's arena. That is
		// bookkeeping this package does for you, not something to hold.
		if err := track.AppendChild(clip.Node); err != nil {
			log.Fatal(err)
		}
	}

	// Three seconds of picture, written as canonical OpenTimelineIO JSON.
	duration, _ := track.Duration()
	fmt.Println(duration.ToSeconds())
	if err := otio.Save(timeline.Node, "cut.otio"); err != nil {
		log.Fatal(err)
	}
}
```

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

Objects are built on their own with `New` functions and put together
afterwards. The schemas are Go types that embed one another the way the
schemas derive from one another, so a `Clip` has every method of `Item`,
`Composable` and `Node`, and `clip.Node` is what a call taking any object
wants. Ask a node what it really is with its `As` method:

```go
if clip, ok := node.AsClip(); ok {
	reference, err := clip.MediaReference("")
}
```

## How objects and documents work here

The core keeps its objects in arenas and an object is an index into one. This
package does that bookkeeping, so there is no document to hold: a new object
gets an arena of its own, and putting it into a timeline moves it there.

A timeline is released when it is collected, so `Close` is not required. It is
worth calling anyway, because it frees a whole timeline at once and at a
moment you chose. A handle into a timeline that has been closed goes stale
rather than dangling, and reports `StatusStaleHandle`.

Appending or inserting an object that is still a child in another timeline is
refused as the library refuses it, with `StatusCoreError` and the library's
own message. Placing an object whose handle has gone stale is refused with
`StatusStaleHandle`. Both refusals come before the object's timeline is
brought over, so both timelines stay whole.

## Errors and missing values

Failure is a Go `error`. Two sentinels are worth knowing.

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 error is
`ErrNoValue`. It means the question was answered, not that something went
wrong:

```go
span, err := clip.SourceRange()
if errors.Is(err, otio.ErrNoValue) {
	// the clip is untrimmed
}
```

A call that only *names* an object rather than placing one refuses one from
another timeline before the library is asked, with `ErrOtherTimeline`.
Detaching a child that belongs to another timeline is a mistake, not an
instruction to merge the two:

```go
if err := track.DetachChild(fromSomewhereElse); errors.Is(err, otio.ErrOtherTimeline) {
	// it was never in this track
}
```

`ErrOtherTimeline` carries no `Status`, because the library was never asked.
Asking an object for something it does not have fails rather than answering
with a zero value: a clip asked for a track's kind returns an error saying so.
An absent string is the empty string, since the core draws no distinction
between an unset name and an empty one.

## Editing and algorithms

```go
package main

import (
	"fmt"
	"log"
	"strings"

	otio "github.com/alchemist-editor/otio-rust/sdk/go"
)

// second makes one second of picture, named.
func second(name string) otio.Clip {
	clip, err := otio.NewClip(name)
	if err != nil {
		log.Fatal(err)
	}
	if err := clip.SetSourceRange(otio.TimeRange{
		StartTime: otio.RationalTime{Value: 0, Rate: 24},
		Duration:  otio.RationalTime{Value: 24, Rate: 24},
	}); err != nil {
		log.Fatal(err)
	}
	return clip
}

func show(track otio.Track) {
	children, err := track.Children()
	if err != nil {
		log.Fatal(err)
	}
	names := make([]string, 0, len(children))
	for _, child := range children {
		name, _ := child.Name()
		names = append(names, name)
	}
	duration, _ := track.Duration()
	fmt.Printf("%s - %v frames\n", strings.Join(names, " "), duration.Value)
}

func main() {
	track, err := otio.NewTrack("V1", "Video")
	if err != nil {
		log.Fatal(err)
	}
	for _, name := range []string{"A", "B", "C"} {
		if err := track.AppendChild(second(name).Node); err != nil {
			log.Fatal(err)
		}
	}
	show(track)

	// Insert makes room: everything from the insertion point onwards moves
	// later, and the track gets longer.
	at := otio.RationalTime{Value: 24, Rate: 24}
	if err := otio.Insert(second("D").Node, track.Node, at, false, nil); err != nil {
		log.Fatal(err)
	}
	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.
	over := otio.TimeRange{
		StartTime: otio.RationalTime{Value: 48, Rate: 24},
		Duration:  otio.RationalTime{Value: 24, Rate: 24},
	}
	if err := otio.Overwrite(second("E").Node, track.Node, over, false, nil); err != nil {
		log.Fatal(err)
	}
	show(track)
}
```

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

The algorithms and the ten edit operations are functions of the package rather
than methods, because each is about two objects and belongs to neither, which
is how upstream arranges them too: `otio.Insert`, `otio.Overwrite` and the
rest, beside `otio.FlattenStack`, `otio.FlattenTracks` and
`otio.TrackTrimmedToRange`. [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:
`FormatOTIOJSON`, `FormatALE`, `FormatCMX3600`, `FormatFcp7XML`,
`FormatFcpxXML`, `FormatAAF`, and the two bundles, `FormatOTIOZ` and
`FormatOTIOD`. A bundle is read and written through a path only.

```go
package main

import (
	"fmt"
	"log"

	otio "github.com/alchemist-editor/otio-rust/sdk/go"
)

func main() {
	timeline, err := otio.NewTimeline("Cut")
	if err != nil {
		log.Fatal(err)
	}
	defer timeline.Close()

	tracks, err := timeline.Tracks()
	if err != nil {
		log.Fatal(err)
	}
	stack, _ := tracks.AsStack()
	track, err := otio.NewTrack("V1", "Video")
	if err != nil {
		log.Fatal(err)
	}
	if err := stack.AppendChild(track.Node); err != nil {
		log.Fatal(err)
	}

	// A cut of two clips: one whose media is a file beside the program, and
	// one whose media is on the web.
	for _, source := range []struct{ name, url string }{
		{"A001C003", "shot.mov"},
		{"A001C004", "https://example.com/remote.mov"},
	} {
		media, err := otio.NewExternalReference("", source.url)
		if err != nil {
			log.Fatal(err)
		}
		clip, err := otio.NewClip(source.name)
		if err != nil {
			log.Fatal(err)
		}
		if err := clip.SetMediaReference("DEFAULT_MEDIA", media.Node); err != nil {
			log.Fatal(err)
		}
		if err := clip.SetActiveMediaReferenceKey("DEFAULT_MEDIA"); err != nil {
			log.Fatal(err)
		}
		if err := track.AppendChild(clip.Node); err != nil {
			log.Fatal(err)
		}
	}

	// 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. FormatOTIOD writes the
	// same layout as a directory.
	options := &otio.WriteOptions{BundleMediaPolicy: otio.BundleMediaPolicyMissingIfNotFile}
	if err := otio.WriteToFile(otio.FormatOTIOZ, timeline.Node, "cut.otioz", options); err != nil {
		log.Fatal(err)
	}

	// Unpacked, with each reference made absolute, the media is ready to use.
	read := &otio.ReadOptions{BundleExtractPath: "cut", BundleAbsoluteMediaPaths: true}
	root, err := otio.ReadFromFile(otio.FormatOTIOZ, "cut.otioz", read)
	if err != nil {
		log.Fatal(err)
	}
	defer root.Close()

	clips, err := root.FindClips()
	if err != nil {
		log.Fatal(err)
	}
	for _, clip := range otio.Filter(clips, otio.Node.AsClip) {
		media, err := clip.MediaReference("")
		if err != nil {
			log.Fatal(err)
		}
		if external, ok := media.AsExternalReference(); ok {
			url, _ := external.TargetURL()
			fmt.Println(url)
		} else {
			fmt.Println("missing")
		}
	}
}
```

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

A media linker is a `func(clip otio.Clip, arguments otio.Metadata) (otio.Node, error)`
registered under a name with `otio.RegisterMediaLinker`; a read names it in
`ReadOptions.MediaLinker`. Return the zero `Node` to leave a clip alone. A hook
script is a `func(target otio.Node, arguments otio.Metadata) (otio.Node, error)`
registered with `otio.RegisterHookScript` and attached to a hook with
`otio.AttachHookScript`. The arguments arrive as metadata; a read carries
them as JSON in `MediaLinkerArguments` and `HookArguments`.

A returned error, or a panic, fails the read with `StatusPluginError` and the
function's own message. The objects a plugin is handed belong to the read in
progress, so keep none of them past the call. See
[Media linkers and hooks](https://otio-rust-docs.vercel.app/docs/guides/media-linkers-and-hooks/).

## Platforms

Linux and macOS, on whatever architectures the Rust core builds for. Windows
is not tested: linking a Rust static library with MSVC needs an environment CI
does not have set up.

## Reference

- [`sdk/go/README.md`](https://github.com/alchemist-editor/otio-rust/blob/main/sdk/go/README.md).
- The package documentation: every method carries the C ABI's own
  documentation, rewritten for Go, and names the C function it calls.
- 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 where it departs from upstream's shape.

---

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