# 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 <OpenTimelineIO/OTIOExtOri.h>` | `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.

Rust:

```rust
use opentime::RationalTime;
use otio_ext_ori_annotations::annotations;
use otio_ext_ori_annotations::command::{self, Command, PaintStart, TextAnnotation, Vertices};
use otio_ext_ori_annotations::coords::ImageSize;

// A cut with one shot, which sync knows by the GUID `shot`.
const CUT: &str = r#"{
  "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"}}}]}]}
}"#;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ORI Annotations is an extension; enabling it enables sync too.
    otio_ext_ori_annotations::install();
    otio_ext::enable(otio_ext_ori_annotations::ID)?;

    let mut document = otio_core::from_str(CUT)?;
    let timeline = document.root().ok_or("the cut has no root")?;

    // The Annotations track, and the clip on it for frame 10 of the shot.
    let track = annotations::ensure_track(&mut document, timeline)?;
    let frame = annotations::ensure_clip(&mut document, track, "shot", RationalTime::new(10.0, 24.0))?;

    // ORI's coordinates are height-normalised with the origin at the centre,
    // so convert from the pixels of the 1920x1080 picture.
    let hd = ImageSize::new(1920.0, 1080.0);
    let points: Vec<(f64, f64)> = [(200.0, 200.0), (960.0, 540.0), (1720.0, 880.0)]
        .into_iter()
        .map(|(x, y)| hd.pixels_to_ori(x, y))
        .collect();

    // A stroke is a PaintStart, its points and a PaintEnd sharing one uuid.
    let stroke = command::stroke(
        PaintStart {
            rgba: Some([1.0, 0.25, 0.25, 1.0]),
            participant_hash: Some("reviewer-1".into()),
            timestamp: Some(command::timestamp_now()),
            ..PaintStart::new(otio_ext_sync::guid::new_guid())
        },
        Vertices::with_size(&points, 0.01),
    );
    annotations::add_commands(&mut document, frame, &stroke)?;

    let (x, y) = hd.pixels_to_ori(960.0, 620.0);
    let label = Command::Text(TextAnnotation {
        text: Some("Eyeline is late".into()),
        position: Some([x, y]),
        rgba: Some([1.0, 1.0, 1.0, 1.0]),
        ..TextAnnotation::new(otio_ext_sync::guid::new_guid())
    });
    annotations::add_commands(&mut document, frame, &[label])?;

    // Read them back: every annotation on the shot, frame by frame.
    for annotation in annotations::for_clip(&document, timeline, "shot")? {
        println!("{:?}: {} commands", annotation.frame, annotation.commands.len());
    }

    // The file is plain OpenTimelineIO with ORI's schemas in it, as ORI's
    // own Python writes it.
    println!("{}", otio_core::to_string_pretty(&document, 4)?);
    Ok(())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/annotate-a-frame/rust.rs</sub>

Python:

```python
import json

import opentimelineio as otio
from opentimelineio.ext import ori

# A cut with one shot, which sync knows by the GUID `shot`.
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"}}}]}]}
}"""

# ORI Annotations is an extension; enabling it enables sync too.
ori.enable()
timeline = otio.adapters.read_from_string(CUT, "otio_json")

# The Annotations track, and the clip on it for frame 10 of the shot.
track = ori.ensure_track(timeline)
frame = ori.ensure_clip(track, "shot", otio.opentime.RationalTime(10, 24))

# ORI's coordinates are height-normalised with the origin at the centre,
# so convert from the pixels of the 1920x1080 picture.
points = [ori.pixels_to_ori(1920, 1080, px, py) for px, py in [(200, 200), (960, 540), (1720, 880)]]

# A stroke is a PaintStart, its points and a PaintEnd sharing one uuid.
# Fields left out take the defaults ORI's classes give them.
uuid = ori.new_uuid()
ori.add_commands(frame, [
    {"OTIO_SCHEMA": "PaintStart.1", "uuid": uuid, "rgba": [1.0, 0.25, 0.25, 1.0],
     "participant_hash": "reviewer-1", "timestamp": ori.timestamp_now()},
    {"OTIO_SCHEMA": "PaintPoint.1", "uuid": uuid, "points": {
        "OTIO_SCHEMA": "PaintVertices.1",
        "x": [x for x, _ in points], "y": [y for _, y in points],
        "size": [0.01] * len(points), "alpha": [1.0] * len(points)}},
    {"OTIO_SCHEMA": "PaintEnd.1", "uuid": uuid},
])

# Read them back: every annotation on the shot.
for holder in ori.list_for_clip(timeline, "shot"):
    about = ori.describe(holder)
    print(f"frame {about['frame']['value']}: {about['command_count']} commands")
    print(json.dumps(json.loads(ori.commands(holder))[0], indent=2))

# The file is plain OpenTimelineIO with ORI's schemas in it.
print(otio.adapters.write_to_string(timeline))
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/annotate-a-frame/python.py</sub>

TypeScript:

```typescript
import { init, Timeline, deserializeJsonFromString } from "@alchemist-edit/otio";
import * as ori from "@alchemist-edit/otio/ext/ori";

// A cut with one shot, which sync knows by the GUID `shot`.
const 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"}}}]}]}
}`;

await init();
// ORI Annotations is an extension; enabling it enables sync too.
ori.enable();
const timeline = deserializeJsonFromString(CUT) as Timeline;

// The Annotations track, and the clip on it for frame 10 of the shot.
const track = ori.ensureTrack(timeline);
const frame = ori.ensureClip(track, "shot", 10, 24);

// ORI's coordinates are height-normalised with the origin at the centre,
// so convert from the pixels of the 1920x1080 picture.
const points = [[200, 200], [960, 540], [1720, 880]].map(([px, py]) => ori.pixelsToOri(1920, 1080, px!, py!));

// A stroke is a PaintStart, its points and a PaintEnd sharing one uuid.
// Fields left out take the defaults ORI's classes give them.
const uuid = ori.newUuid();
ori.addCommands(frame, JSON.stringify([
  { OTIO_SCHEMA: "PaintStart.1", uuid, rgba: [1, 0.25, 0.25, 1], participant_hash: "reviewer-1" },
  {
    OTIO_SCHEMA: "PaintPoint.1", uuid, points: {
      OTIO_SCHEMA: "PaintVertices.1",
      x: points.map((point) => point.x), y: points.map((point) => point.y),
      size: points.map(() => 0.01), alpha: points.map(() => 1),
    },
  },
  { OTIO_SCHEMA: "PaintEnd.1", uuid },
]));

// Read them back: every annotation on the shot.
for (const holder of ori.listForClip(timeline, "shot")) {
  console.log(ori.describe(holder));
}

// The file is plain OpenTimelineIO with ORI's schemas in it.
console.log(timeline.toJson(4));
```

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

Go:

```go
package main

import (
	"encoding/json"
	"fmt"
	"log"

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

// A cut with one shot, which sync knows by the GUID `shot`.
const 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"}}}]}]}
}`

func main() {
	// ORI Annotations is an extension; enabling it enables sync too.
	if err := ori.Enable(); err != nil {
		log.Fatal(err)
	}
	timeline, err := otio.FromJSON(cut)
	if err != nil {
		log.Fatal(err)
	}
	defer timeline.Close()

	// The Annotations track, and the clip on it for frame 10 of the shot.
	track, err := ori.EnsureTrack(timeline)
	if err != nil {
		log.Fatal(err)
	}
	frame, err := ori.EnsureClip(track, "shot", 10, 24)
	if err != nil {
		log.Fatal(err)
	}

	// ORI's coordinates are height-normalised with the origin at the
	// centre, so convert from the pixels of the 1920x1080 picture.
	var xs, ys, sizes, alphas []float64
	for _, pixel := range [][2]float64{{200, 200}, {960, 540}, {1720, 880}} {
		x, y, err := ori.PixelsToOri(1920, 1080, pixel[0], pixel[1])
		if err != nil {
			log.Fatal(err)
		}
		xs, ys = append(xs, x), append(ys, y)
		sizes, alphas = append(sizes, 0.01), append(alphas, 1)
	}

	// A stroke is a PaintStart, its points and a PaintEnd sharing one uuid.
	// Fields left out take the defaults ORI's classes give them.
	uuid, err := ori.NewUUID()
	if err != nil {
		log.Fatal(err)
	}
	stroke, err := json.Marshal([]map[string]any{
		{"OTIO_SCHEMA": "PaintStart.1", "uuid": uuid, "rgba": []float64{1, 0.25, 0.25, 1},
			"participant_hash": "reviewer-1"},
		{"OTIO_SCHEMA": "PaintPoint.1", "uuid": uuid, "points": map[string]any{
			"OTIO_SCHEMA": "PaintVertices.1", "x": xs, "y": ys, "size": sizes, "alpha": alphas}},
		{"OTIO_SCHEMA": "PaintEnd.1", "uuid": uuid},
	})
	if err != nil {
		log.Fatal(err)
	}
	if err := ori.AddCommands(frame, string(stroke)); err != nil {
		log.Fatal(err)
	}

	// Read them back: every annotation on the shot.
	holders, err := ori.ListForClip(timeline, "shot")
	if err != nil {
		log.Fatal(err)
	}
	for _, holder := range holders {
		about, err := ori.Describe(holder)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(about)
	}

	// The file is plain OpenTimelineIO with ORI's schemas in it.
	written, err := timeline.ToJSON(4)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(written)
}
```

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

Swift:

```swift
import Foundation
import OTIOExtOri
import OpenTimelineIO

// A cut with one shot, which sync knows by the GUID `shot`.
let 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"}}}]}]}}
    """

// ORI Annotations is an extension; enabling it enables sync too.
try Ori.enable()
let timeline = try OTIO.fromJSON(cut)

// The Annotations track, and the clip on it for frame 10 of the shot.
let track = try Ori.ensureTrack(timeline)
let frame = try Ori.ensureClip(track, clipGuid: "shot", value: 10, rate: 24)

// ORI's coordinates are height-normalised with the origin at the centre,
// so convert from the pixels of the 1920x1080 picture.
let points = try [(200.0, 200.0), (960.0, 540.0), (1720.0, 880.0)].map {
    try Ori.pixelsToOri(1920, height: 1080, px: $0.0, py: $0.1)
}

// A stroke is a PaintStart, its points and a PaintEnd sharing one uuid.
// Fields left out take the defaults ORI's classes give them.
let uuid = try Ori.newUUID()
let stroke: [[String: Any]] = [
    ["OTIO_SCHEMA": "PaintStart.1", "uuid": uuid, "rgba": [1, 0.25, 0.25, 1],
     "participant_hash": "reviewer-1"],
    ["OTIO_SCHEMA": "PaintPoint.1", "uuid": uuid, "points": [
        "OTIO_SCHEMA": "PaintVertices.1",
        "x": points.map(\.x), "y": points.map(\.y),
        "size": points.map { _ in 0.01 }, "alpha": points.map { _ in 1.0 },
    ]],
    ["OTIO_SCHEMA": "PaintEnd.1", "uuid": uuid],
]
let json = String(decoding: try JSONSerialization.data(withJSONObject: stroke), as: UTF8.self)
try Ori.addCommands(frame, commandsJSON: json)

// Read them back: every annotation on the shot.
for holder in try Ori.listForClip(timeline, clipGuid: "shot") {
    print(try Ori.describe(holder))
}

// The file is plain OpenTimelineIO with ORI's schemas in it.
print(try timeline.toJSON(4))
```

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

C++:

```cpp
#include <iostream>
#include <sstream>
#include <string>

#include <opentimelineio/ext/ori.hpp>
#include <opentimelineio/otio.hpp>

namespace ori = otio::ext::ori;

// A cut with one shot, which sync knows by the GUID `shot`.
const char *const CUT = R"({
  "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"}}}]}]}
})";

int main() {
    // ORI Annotations is an extension; enabling it enables sync too.
    ori::enable();
    const otio::SerializableObject timeline = otio::from_json(CUT);

    // The Annotations track, and the clip on it for frame 10 of the shot.
    const otio::SerializableObject track = ori::ensure_track(timeline);
    const otio::SerializableObject frame = ori::ensure_clip(track, "shot", 10, 24);

    // ORI's coordinates are height-normalised with the origin at the
    // centre, so convert from the pixels of the 1920x1080 picture.
    std::ostringstream xs, ys;
    xs.precision(17);
    ys.precision(17);
    const double pixels[3][2] = {{200, 200}, {960, 540}, {1720, 880}};
    for (int i = 0; i < 3; i++) {
        const auto point = ori::pixels_to_ori(1920, 1080, pixels[i][0], pixels[i][1]);
        xs << (i ? "," : "") << point.x;
        ys << (i ? "," : "") << point.y;
    }

    // A stroke is a PaintStart, its points and a PaintEnd sharing one uuid.
    // Fields left out take the defaults ORI's classes give them.
    const std::string uuid = ori::new_uuid();
    ori::add_commands(frame,
        R"([{"OTIO_SCHEMA":"PaintStart.1","uuid":")" + uuid +
        R"(","rgba":[1,0.25,0.25,1],"participant_hash":"reviewer-1"},)"
        R"({"OTIO_SCHEMA":"PaintPoint.1","uuid":")" + uuid +
        R"(","points":{"OTIO_SCHEMA":"PaintVertices.1","x":[)" + xs.str() + R"(],"y":[)" + ys.str() +
        R"(],"size":[0.01,0.01,0.01],"alpha":[1,1,1]}},)"
        R"({"OTIO_SCHEMA":"PaintEnd.1","uuid":")" + uuid + R"("}])");

    // Read them back: every annotation on the shot.
    for (const otio::SerializableObject &holder : ori::list_for_clip(timeline, "shot")) {
        std::cout << ori::describe(holder) << "\n";
    }

    // The file is plain OpenTimelineIO with ORI's schemas in it.
    std::cout << timeline.to_json(4) << "\n";
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/annotate-a-frame/cpp.cpp</sub>

Zig:

```zig
const std = @import("std");
const otio = @import("otio");
const ori = otio.ext.ori;

// A cut with one shot, which sync knows by the GUID `shot`.
const 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"}}}]}]}}
;

pub fn main(init: std.process.Init) !void {
    const allocator = init.arena.allocator();

    // ORI Annotations is an extension; enabling it enables sync too.
    try ori.enable();
    const document = try otio.Document.fromJson(cut);
    defer document.deinit();
    const timeline = (try document.root()).?;

    // The Annotations track, and the clip on it for frame 10 of the shot.
    const track = try ori.ensureTrack(document, timeline);
    const frame = try ori.ensureClip(document, track, "shot", 10, 24);

    // ORI's coordinates are height-normalised with the origin at the
    // centre, so convert from the pixels of the 1920x1080 picture.
    const pixels = [_][2]f64{ .{ 200, 200 }, .{ 960, 540 }, .{ 1720, 880 } };
    var xs: [pixels.len]f64 = undefined;
    var ys: [pixels.len]f64 = undefined;
    for (pixels, 0..) |pixel, i| {
        const point = try ori.pixelsToOri(1920, 1080, pixel[0], pixel[1]);
        xs[i] = point.x;
        ys[i] = point.y;
    }

    // A stroke is a PaintStart, its points and a PaintEnd sharing one uuid.
    // Fields left out take the defaults ORI's classes give them.
    const uuid = try ori.newUuid(allocator);
    const stroke = try std.fmt.allocPrintSentinel(allocator,
        \\[{{"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":[{d},{d},{d}],"y":[{d},{d},{d}],"size":[0.01,0.01,0.01],"alpha":[1,1,1]}}}},
        \\ {{"OTIO_SCHEMA":"PaintEnd.1","uuid":"{s}"}}]
    , .{ uuid, uuid, xs[0], xs[1], xs[2], ys[0], ys[1], ys[2], uuid }, 0);
    try ori.addCommands(document, frame, stroke);

    // Read them back: every annotation on the shot.
    for (try ori.listForClip(document, allocator, timeline, "shot")) |holder| {
        std.debug.print("{s}\n", .{try ori.describe(document, allocator, holder)});
    }

    // The file is plain OpenTimelineIO with ORI's schemas in it.
    std.debug.print("{s}\n", .{try timeline.toJson(allocator, 4)});
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/annotate-a-frame/zig.zig</sub>

C:

```c
#include <stdio.h>

#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;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/annotate-a-frame/c.c</sub>

C#:

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using OpenTimelineIO;
using OpenTimelineIO.Ext.Ori;

// A cut with one shot, which sync knows by the GUID `shot`.
const string 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"}}}]}]}
    }
    """;

// ORI Annotations is an extension; enabling it enables sync too.
OriExtension.Enable();
var timeline = Otio.FromJson(Cut);

// The Annotations track, and the clip on it for frame 10 of the shot.
var track = OriExtension.EnsureTrack(timeline);
var frame = OriExtension.EnsureClip(track, "shot", 10, 24);

// ORI's coordinates are height-normalised with the origin at the centre,
// so convert from the pixels of the 1920x1080 picture.
var points = new[] { (200.0, 200.0), (960.0, 540.0), (1720.0, 880.0) }
    .Select(pixel => OriExtension.PixelsToOri(1920, 1080, pixel.Item1, pixel.Item2))
    .ToArray();

// A stroke is a PaintStart, its points and a PaintEnd sharing one uuid.
// Fields left out take the defaults ORI's classes give them.
var uuid = OriExtension.NewUuid();
var stroke = JsonSerializer.Serialize(new object[]
{
    new Dictionary<string, object>
    {
        ["OTIO_SCHEMA"] = "PaintStart.1", ["uuid"] = uuid,
        ["rgba"] = new[] { 1.0, 0.25, 0.25, 1.0 }, ["participant_hash"] = "reviewer-1",
    },
    new Dictionary<string, object>
    {
        ["OTIO_SCHEMA"] = "PaintPoint.1", ["uuid"] = uuid,
        ["points"] = new Dictionary<string, object>
        {
            ["OTIO_SCHEMA"] = "PaintVertices.1",
            ["x"] = points.Select(point => point.x).ToArray(),
            ["y"] = points.Select(point => point.y).ToArray(),
            ["size"] = points.Select(_ => 0.01).ToArray(),
            ["alpha"] = points.Select(_ => 1.0).ToArray(),
        },
    },
    new Dictionary<string, object> { ["OTIO_SCHEMA"] = "PaintEnd.1", ["uuid"] = uuid },
});
OriExtension.AddCommands(frame, stroke);

// Read them back: every annotation on the shot.
foreach (var holder in OriExtension.ListForClip(timeline, "shot"))
{
    Console.WriteLine(OriExtension.Describe(holder));
}

// The file is plain OpenTimelineIO with ORI's schemas in it.
Console.WriteLine(timeline.ToJson(4));
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/annotate-a-frame/csharp.cs</sub>

Objective-C:

```objectivec
#import <Foundation/Foundation.h>

#import <OpenTimelineIO/OTIOExtOri.h>

// A cut with one shot, which sync knows by the GUID `shot`.
static NSString *const 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\"}}}]}]}}";

static int Fail(NSError *error) {
    fprintf(stderr, "%s\n", error.localizedDescription.UTF8String);
    return 1;
}

int main(void) {
    @autoreleasepool {
        NSError *error = nil;

        // ORI Annotations is an extension; enabling it enables sync too.
        if (!OTIOExtOriEnable(&error)) return Fail(error);
        OTIOSerializableObject *timeline = OTIOFromJSON(CUT, &error);
        if (timeline == nil) return Fail(error);

        // The Annotations track, and the clip on it for frame 10 of the shot.
        OTIOSerializableObject *track = OTIOExtOriEnsureTrack(timeline, &error);
        if (track == nil) return Fail(error);
        OTIOSerializableObject *frame = OTIOExtOriEnsureClip(track, @"shot", 10, 24, &error);
        if (frame == nil) return Fail(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}};
        NSMutableArray *xs = [NSMutableArray array];
        NSMutableArray *ys = [NSMutableArray array];
        for (int i = 0; i < 3; i++) {
            double x = 0, y = 0;
            if (!OTIOExtOriPixelsToOri(1920, 1080, pixels[i][0], pixels[i][1], &x, &y, &error)) {
                return Fail(error);
            }
            [xs addObject:@(x)];
            [ys addObject:@(y)];
        }

        // A stroke is a PaintStart, its points and a PaintEnd sharing one
        // uuid. Fields left out take the defaults ORI's classes give them.
        NSString *uuid = OTIOExtOriNewUUID(&error);
        if (uuid == nil) return Fail(error);
        NSArray *stroke = @[
            @{@"OTIO_SCHEMA" : @"PaintStart.1", @"uuid" : uuid, @"rgba" : @[ @1, @0.25, @0.25, @1 ],
              @"participant_hash" : @"reviewer-1"},
            @{@"OTIO_SCHEMA" : @"PaintPoint.1", @"uuid" : uuid,
              @"points" : @{@"OTIO_SCHEMA" : @"PaintVertices.1", @"x" : xs, @"y" : ys,
                            @"size" : @[ @0.01, @0.01, @0.01 ], @"alpha" : @[ @1, @1, @1 ]}},
            @{@"OTIO_SCHEMA" : @"PaintEnd.1", @"uuid" : uuid},
        ];
        NSData *data = [NSJSONSerialization dataWithJSONObject:stroke options:0 error:&error];
        if (data == nil) return Fail(error);
        NSString *json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        if (!OTIOExtOriAddCommands(frame, json, &error)) return Fail(error);

        // Read them back: every annotation on the shot.
        NSArray<OTIOSerializableObject *> *holders = OTIOExtOriListForClip(timeline, @"shot", &error);
        if (holders == nil) return Fail(error);
        for (OTIOSerializableObject *holder in holders) {
            printf("%s\n", OTIOExtOriDescribe(holder, &error).UTF8String);
        }

        // The file is plain OpenTimelineIO with ORI's schemas in it.
        printf("%s\n", [timeline toJSON:4 error:&error].UTF8String);
    }
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/annotate-a-frame/objectivec.m</sub>

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.

Rust:

```rust
use otio_ext_ori_annotations::{annotations, command, review};
use otio_ext_sync::session::{Config, Event, Session};
use otio_ext_sync_transport::{Connection, Frame, Furnace, now_millis};

// The cut both sides have: one shot, which sync knows by the GUID main/shot.
const CUT: &str = r#"{"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_range":{"OTIO_SCHEMA":"TimeRange.1",
    "start_time":{"OTIO_SCHEMA":"RationalTime.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.
const WELCOME: &str = r#"{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","selfUserId":"u_bea","role":"viewer","roster":[],"session":{"id":"s_1","createdAt":0}}}"#;

// Where the reviewer drew: frame 10 of the shot, as the program shows it.
const ANCHOR: &str = r#"{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot",
 "sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},
 "capturedAt":1787742000000}"#;

// One stroke: a PaintStart, its points and a PaintEnd sharing a uuid.
const STROKE: &str = r#"[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},
 {"OTIO_SCHEMA":"PaintPoint.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"}]"#;

// What Furnace tells the host when it opens the session.
const HOST_WELCOME: &str = r#"{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_1","selfUserId":"u_ari","role":"host","roster":[],"session":{"id":"s_1","createdAt":0}}}"#;

// 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.
fn relay() -> String {
    format!(
        r#"{{"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,"revision":1,"createdAt":0,"anchor":{ANCHOR},"bodyMarkdown":"Too dark here.","annotationCommands":{STROKE}}}}}"#
    )
}

// Stands in for the network: a real program writes every frame the session
// has waiting to its WebSocket to Furnace.
fn send(connection: &mut Connection) -> Result<(), otio_ext_sync_transport::Error> {
    while let Some(frame) = connection.poll_transmit(now_millis())? {
        println!("send {} bytes", frame.data.len());
    }
    Ok(())
}

fn joining(token: &str, capability: &[&str]) -> Connection {
    let mut config = Config {
        token: token.to_string(),
        ..Config::default()
    };
    config.identity.capabilities = capability.iter().map(|name| name.to_string()).collect();
    Connection::new(Session::new(config), Furnace::new())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ORI Annotations is an extension; enabling it enables sync too.
    otio_ext_ori_annotations::install();
    otio_ext::enable(otio_ext_ori_annotations::ID)?;

    // The reviewer joins with the right to comment.
    let mut reviewer = joining(
        "join-code",
        &["review.comment.create", "review.annotation.create"],
    );
    reviewer.connect();
    send(&mut reviewer)?;
    reviewer.receive(&Frame::text(WELCOME))?;
    while reviewer.poll_event().is_some() {}

    // Commit the stroke with a note. The session sends it again after every
    // reconnect until Furnace stores it, so it is not lost.
    let seen = otio_core::from_str(CUT)?;
    let id = review::commit(
        reviewer.session_mut(),
        &seen,
        seen.root().ok_or("the cut has no root")?,
        &review::Anchor::from_json(ANCHOR)?,
        "Too dark here.",
        &command::from_json(STROKE)?,
        "",
    )?;
    println!("committed {id}");
    send(&mut reviewer)?;

    // The host hears the stored comment and folds it into its timeline.
    let mut host = joining("host-token", &["review.commit.receive"]);
    host.connect();
    send(&mut host)?;
    host.receive(&Frame::text(HOST_WELCOME))?;
    host.receive(&Frame::text(relay()))?;
    let mut document = otio_core::from_str(CUT)?;
    let timeline = document.root().ok_or("the cut has no root")?;
    while let Some(event) = host.poll_event() {
        // A stroke still being drawn: draw it over the picture, then drop it.
        if let Some(partial) = review::read_partial(&event)? {
            println!("drawing {} commands", partial.commands.len());
            continue;
        }
        let Event::ReviewRelay(relay) = event else {
            continue;
        };
        let folded = review::fold(&mut document, timeline, &relay)?;
        let about = annotations::describe(&document, folded.clip)?;
        println!(
            "folded {} already there: {}",
            about.to_json(&document),
            folded.already
        );
        let owner = review::owner_of(&document, folded.clip, "s1")?;
        println!("the stroke belongs to {owner:?}");
    }
    Ok(())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/share-an-annotation/rust.rs</sub>

Python:

```python
import json

import opentimelineio as otio
from opentimelineio.ext import ori, sync

# The cut both sides have: one shot, which sync knows by the GUID main/shot.
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_range":{"OTIO_SCHEMA":"TimeRange.1",'
    '"start_time":{"OTIO_SCHEMA":"RationalTime.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. And what it tells the host when it opens the session.
WELCOME = (
    '{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{'
    '"selfId":"p_2","selfUserId":"u_bea","role":"viewer","roster":[],'
    '"session":{"id":"s_1","createdAt":0}}}'
)
HOST_WELCOME = (
    '{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{'
    '"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.
ANCHOR = {
    "sequenceId": "main", "capturedGeneration": 1, "clipGuid": "main/shot",
    "sourceTime": {"value": 10, "rate": 24},
    "capturedSequenceTime": {"value": 10, "rate": 24},
    "capturedAt": 1787742000000,
}

# One stroke: a PaintStart, its points and a PaintEnd sharing a uuid.
STROKE = [
    {"OTIO_SCHEMA": "PaintStart.1", "uuid": "s1", "rgba": [1, 0.25, 0.25, 1]},
    {"OTIO_SCHEMA": "PaintPoint.1", "uuid": "s1", "points": {
        "OTIO_SCHEMA": "PaintVertices.1", "x": [-0.5, 0, 0.5], "y": [0.2, 0, -0.2],
        "size": [0.01] * 3, "alpha": [1] * 3}},
    {"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.
RELAY = json.dumps({
    "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, "revision": 1, "createdAt": 0,
        "anchor": ANCHOR, "bodyMarkdown": "Too dark here.", "annotationCommands": STROKE,
    },
})


def send(session):
    # Stands in for the network: a real program writes every frame the
    # session has waiting to its WebSocket to Furnace.
    while (frame := session.poll_transmit()) is not None:
        print("send", len(frame), "characters")


# ORI Annotations is an extension; enabling it enables sync too.
ori.enable()

# The reviewer joins with the right to comment.
reviewer = sync.Session(
    token="join-code",
    identity={"capabilities": ["review.comment.create", "review.annotation.create"]},
)
reviewer.connect()
send(reviewer)
reviewer.receive(WELCOME)
while reviewer.poll_event() is not None:
    pass

# Commit the stroke with a note. The session sends it again after every
# reconnect until Furnace stores it, so it is not lost.
seen = otio.adapters.read_from_string(CUT, "otio_json")
print("committed", ori.commit(reviewer, seen, ANCHOR, "Too dark here.", STROKE))
send(reviewer)

# The host hears the stored comment and folds it into its timeline.
host = sync.Session(token="host-token", identity={"capabilities": ["review.commit.receive"]})
host.connect()
send(host)
host.receive(HOST_WELCOME)
host.receive(RELAY)
timeline = otio.adapters.read_from_string(CUT, "otio_json")
while (event := host.poll_event()) is not None:
    # A stroke still being drawn: draw it over the picture, then drop it.
    partial = ori.read_partial(event)
    if partial is not None:
        print("drawing", len(partial["events"]), "commands")
    elif event["type"] == "review_relay":
        clip, already = ori.fold(timeline, event)
        print("folded", ori.describe(clip), "already there:", already)
        print("the stroke belongs to", ori.owner_of(clip, "s1"))
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/share-an-annotation/python.py</sub>

TypeScript:

```typescript
import { init, Timeline, deserializeJsonFromString } from "@alchemist-edit/otio";
import * as ori from "@alchemist-edit/otio/ext/ori";
import * as sync from "@alchemist-edit/otio/ext/sync";

// The cut both sides have: one shot, which sync knows by the GUID main/shot.
const 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_range":{"OTIO_SCHEMA":"TimeRange.1",
    "start_time":{"OTIO_SCHEMA":"RationalTime.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. And what it tells the host when it opens the session.
const WELCOME = '{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","selfUserId":"u_bea","role":"viewer","roster":[],"session":{"id":"s_1","createdAt":0}}}';
const HOST_WELCOME = '{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
const anchor = {
  sequenceId: "main", capturedGeneration: 1, clipGuid: "main/shot",
  sourceTime: { value: 10, rate: 24 }, capturedSequenceTime: { value: 10, rate: 24 },
  capturedAt: 1787742000000,
};

// One stroke: a PaintStart, its points and a PaintEnd sharing a uuid.
const stroke = [
  { OTIO_SCHEMA: "PaintStart.1", uuid: "s1", rgba: [1, 0.25, 0.25, 1] },
  { OTIO_SCHEMA: "PaintPoint.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" },
];

// 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.
const RELAY = JSON.stringify({
  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, revision: 1, createdAt: 0,
    anchor, bodyMarkdown: "Too dark here.", annotationCommands: stroke,
  },
});

// Stands in for the network: a real program writes every frame the session
// has waiting to its WebSocket to Furnace.
const send = (session: number): void => {
  for (let frame = sync.sessionPollTransmit(session, Date.now()); frame !== undefined;
    frame = sync.sessionPollTransmit(session, Date.now())) {
    console.log(`send ${frame.bytes.length} bytes`);
  }
};
const receive = (session: number, frame: string): void =>
  sync.sessionReceive(session, new TextEncoder().encode(frame), false);

await init();
// ORI Annotations is an extension; enabling it enables sync too.
ori.enable();

// The reviewer joins with the right to comment.
const reviewer = sync.sessionNew(JSON.stringify({
  token: "join-code",
  identity: { capabilities: ["review.comment.create", "review.annotation.create"] },
}));
sync.sessionConnect(reviewer);
send(reviewer);
receive(reviewer, WELCOME);
while (sync.sessionPollEvent(reviewer) !== undefined) {
  // Nothing to show yet.
}

// Commit the stroke with a note. The session sends it again after every
// reconnect until Furnace stores it, so it is not lost.
const seen = deserializeJsonFromString(CUT) as Timeline;
const id = ori.commit(reviewer, seen, JSON.stringify(anchor), "Too dark here.", JSON.stringify(stroke), "");
console.log(`committed ${id}`);
send(reviewer);

// The host hears the stored comment and folds it into its timeline.
const host = sync.sessionNew('{"token":"host-token","identity":{"capabilities":["review.commit.receive"]}}');
sync.sessionConnect(host);
send(host);
receive(host, HOST_WELCOME);
receive(host, RELAY);
const timeline = deserializeJsonFromString(CUT) as Timeline;
for (let event = sync.sessionPollEvent(host); event !== undefined; event = sync.sessionPollEvent(host)) {
  // A stroke still being drawn: draw it over the picture, then drop it.
  const partial = ori.readPartial(event);
  if (partial !== undefined) {
    console.log(`drawing ${partial}`);
  } else if ((JSON.parse(event) as { type: string }).type === "review_relay") {
    const { clip, already } = ori.fold(timeline, event);
    console.log(`folded ${ori.describe(clip)} already there: ${already}`);
    console.log(`the stroke belongs to ${ori.ownerOf(clip, "s1")}`);
  }
}
sync.sessionClose(reviewer);
sync.sessionClose(host);
```

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

Go:

```go
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"time"

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

// The cut both sides have: one shot, which sync knows by the GUID main/shot.
const 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_range":{"OTIO_SCHEMA":"TimeRange.1",
    "start_time":{"OTIO_SCHEMA":"RationalTime.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.
const welcome = `{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","selfUserId":"u_bea","role":"viewer","roster":[],"session":{"id":"s_1","createdAt":0}}}`

// Where the reviewer drew: frame 10 of the shot, as the program shows it.
const anchor = `{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot",
 "sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},
 "capturedAt":1787742000000}`

// One stroke: a PaintStart, its points and a PaintEnd sharing a uuid.
const stroke = `[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},
 {"OTIO_SCHEMA":"PaintPoint.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"}]`

// What Furnace tells the host when it opens the session.
const hostWelcome = `{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_1","selfUserId":"u_ari","role":"host","roster":[],"session":{"id":"s_1","createdAt":0}}}`

// 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.
const 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,"revision":1,"createdAt":0,"anchor":` + anchor + `,"bodyMarkdown":"Too dark here.","annotationCommands":` + stroke + `}}`

// sendAll stands in for the network: a real program writes every frame the
// session has waiting to its WebSocket to Furnace.
func sendAll(session uint64) {
	for {
		frame, _, err := sync.SessionPollTransmit(session, time.Now().UnixMilli())
		if errors.Is(err, otio.ErrNoValue) {
			return
		}
		if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("send %d bytes\n", len(frame))
	}
}

// events is every event the session has for the program now.
func events(session uint64) []string {
	var all []string
	for {
		event, err := sync.SessionPollEvent(session)
		if errors.Is(err, otio.ErrNoValue) {
			return all
		}
		if err != nil {
			log.Fatal(err)
		}
		all = append(all, event)
	}
}

func main() {
	// ORI Annotations is an extension; enabling it enables sync too.
	if err := ori.Enable(); err != nil {
		log.Fatal(err)
	}

	// The reviewer joins with the right to comment.
	reviewer, err := sync.SessionNew(`{"token":"join-code","identity":{"capabilities":["review.comment.create","review.annotation.create"]}}`)
	if err != nil {
		log.Fatal(err)
	}
	if err := sync.SessionConnect(reviewer); err != nil {
		log.Fatal(err)
	}
	sendAll(reviewer)
	if err := sync.SessionReceive(reviewer, []byte(welcome), false); err != nil {
		log.Fatal(err)
	}
	events(reviewer)

	// Commit the stroke with a note. The session sends it again after every
	// reconnect until Furnace stores it, so it is not lost.
	seen, err := otio.FromJSON(cut)
	if err != nil {
		log.Fatal(err)
	}
	defer seen.Close()
	id, err := ori.Commit(reviewer, seen, anchor, "Too dark here.", stroke, "")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("committed", id)
	sendAll(reviewer)

	// The host hears the stored comment and folds it into its timeline.
	host, err := sync.SessionNew(`{"token":"host-token","identity":{"capabilities":["review.commit.receive"]}}`)
	if err != nil {
		log.Fatal(err)
	}
	if err := sync.SessionConnect(host); err != nil {
		log.Fatal(err)
	}
	sendAll(host)
	for _, frame := range []string{hostWelcome, relay} {
		if err := sync.SessionReceive(host, []byte(frame), false); err != nil {
			log.Fatal(err)
		}
	}
	timeline, err := otio.FromJSON(cut)
	if err != nil {
		log.Fatal(err)
	}
	defer timeline.Close()
	for _, event := range events(host) {
		// A stroke still being drawn: draw it over the picture, then drop it.
		if partial, err := ori.ReadPartial(event); err == nil {
			fmt.Println("drawing", partial)
			continue
		}
		var kind struct {
			Type string `json:"type"`
		}
		if err := json.Unmarshal([]byte(event), &kind); err != nil || kind.Type != "review_relay" {
			continue
		}
		clip, already, err := ori.Fold(timeline, event)
		if err != nil {
			log.Fatal(err)
		}
		about, err := ori.Describe(clip)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("folded", about, "already there:", already)
		owner, err := ori.OwnerOf(clip, "s1")
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("the stroke belongs to", owner)
	}

	for _, session := range []uint64{reviewer, host} {
		if err := sync.SessionClose(session); err != nil {
			log.Fatal(err)
		}
	}
}
```

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

Swift:

```swift
import Foundation
import OTIOExtOri
import OTIOExtSync
import OpenTimelineIO

// The cut both sides have: one shot, which sync knows by the GUID
// main/shot.
let 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_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.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.
let welcome = #"{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
let hostWelcome = #"{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
let anchor = #"{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot","sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},"capturedAt":1787742000000}"#

// One stroke: a PaintStart, its points and a PaintEnd sharing a uuid.
let stroke = #"[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},{"OTIO_SCHEMA":"PaintPoint.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"}]"#

// 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.
let 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,"revision":1,"createdAt":0,"anchor":{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot","sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},"capturedAt":1787742000000},"bodyMarkdown":"Too dark here.","annotationCommands":[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},{"OTIO_SCHEMA":"PaintPoint.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"}]}}"#

// Stands in for the network: a real program writes every frame the session
// has waiting to its WebSocket to Furnace.
func sendFrames(_ session: UInt64) throws {
    while let frame = try Sync.sessionPollTransmit(session, now: 0) {
        print("send \(frame.bytes.count) bytes")
    }
}

// ORI Annotations is an extension; enabling it enables sync too.
try Ori.enable()

// The reviewer joins with the right to comment.
let reviewer = try Sync.sessionNew(
    #"{"token":"join-code","identity":{"capabilities":["review.comment.create","review.annotation.create"]}}"#)
try Sync.sessionConnect(reviewer)
try sendFrames(reviewer)
try Sync.sessionReceive(reviewer, data: Array(welcome.utf8), binary: false)
while try Sync.sessionPollEvent(reviewer) != nil {}

// Commit the stroke with a note. The session sends it again after every
// reconnect until Furnace stores it, so it is not lost.
let seen = try OTIO.fromJSON(cut)
let id = try Ori.commit(
    reviewer, timeline: seen, anchorJSON: anchor, bodyMarkdown: "Too dark here.",
    commandsJSON: stroke, clientMutationID: "")
print("committed \(id)")
try sendFrames(reviewer)

// The host hears the stored comment and folds it into its timeline.
let host = try Sync.sessionNew(#"{"token":"host-token","identity":{"capabilities":["review.commit.receive"]}}"#)
try Sync.sessionConnect(host)
try sendFrames(host)
for frame in [hostWelcome, relay] {
    try Sync.sessionReceive(host, data: Array(frame.utf8), binary: false)
}
let timeline = try OTIO.fromJSON(cut)
while let event = try Sync.sessionPollEvent(host) {
    // A stroke still being drawn: draw it over the picture, then drop it.
    if let partial = try Ori.readPartial(event) {
        print("drawing \(partial)")
        continue
    }
    let fields = try JSONSerialization.jsonObject(with: Data(event.utf8)) as? [String: Any]
    guard fields?["type"] as? String == "review_relay" else { continue }
    let folded = try Ori.fold(timeline, eventJSON: event)
    print("folded \(try Ori.describe(folded.clip)) already there: \(folded.already)")
    print("the stroke belongs to \(try Ori.ownerOf(folded.clip, uuid: "s1") ?? "nobody")")
}
try Sync.sessionClose(reviewer)
try Sync.sessionClose(host)
```

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

C++:

```cpp
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>

#include <opentimelineio/ext/ori.hpp>
#include <opentimelineio/ext/sync.hpp>
#include <opentimelineio/otio.hpp>

namespace ori = otio::ext::ori;
namespace sync = otio::ext::sync;

// The cut both sides have: one shot, which sync knows by the GUID
// main/shot.
const char *const CUT = R"({"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_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.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.
const char *const WELCOME = R"({"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
const char *const HOST_WELCOME = R"({"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
const char *const ANCHOR = R"({"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot","sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},"capturedAt":1787742000000})";

// One stroke: a PaintStart, its points and a PaintEnd sharing a uuid.
const char *const STROKE = R"([{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},{"OTIO_SCHEMA":"PaintPoint.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"}])";

// 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.
const char *const RELAY = R"({"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,"revision":1,"createdAt":0,"anchor":{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot","sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},"capturedAt":1787742000000},"bodyMarkdown":"Too dark here.","annotationCommands":[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},{"OTIO_SCHEMA":"PaintPoint.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"}]}})";

// Stands in for the network: a real program writes every frame the session
// has waiting to its WebSocket to Furnace.
void send_frames(std::uint64_t session) {
    while (const auto frame = sync::session_poll_transmit(session, 0)) {
        std::cout << "send " << frame->bytes.size() << " bytes\n";
    }
}

void receive(std::uint64_t session, const std::string &frame) {
    sync::session_receive(session, std::vector<std::uint8_t>(frame.begin(), frame.end()), false);
}

int main() {
    // ORI Annotations is an extension; enabling it enables sync too.
    ori::enable();

    // The reviewer joins with the right to comment.
    const std::uint64_t reviewer = sync::session_new(
        R"({"token":"join-code","identity":{"capabilities":["review.comment.create","review.annotation.create"]}})");
    sync::session_connect(reviewer);
    send_frames(reviewer);
    receive(reviewer, WELCOME);
    while (sync::session_poll_event(reviewer)) {
    }

    // Commit the stroke with a note. The session sends it again after every
    // reconnect until Furnace stores it, so it is not lost.
    const otio::SerializableObject seen = otio::from_json(CUT);
    std::cout << "committed " << ori::commit(reviewer, seen, ANCHOR, "Too dark here.", STROKE, "")
              << "\n";
    send_frames(reviewer);

    // The host hears the stored comment and folds it into its timeline.
    const std::uint64_t host =
        sync::session_new(R"({"token":"host-token","identity":{"capabilities":["review.commit.receive"]}})");
    sync::session_connect(host);
    send_frames(host);
    receive(host, HOST_WELCOME);
    receive(host, RELAY);
    const otio::SerializableObject timeline = otio::from_json(CUT);
    while (const auto event = sync::session_poll_event(host)) {
        // A stroke still being drawn: draw it over the picture, then drop it.
        if (const auto partial = ori::read_partial(*event)) {
            std::cout << "drawing " << *partial << "\n";
        } else if (event->find(R"("type":"review_relay")") != std::string::npos) {
            const ori::FoldResult folded = ori::fold(timeline, *event);
            std::cout << "folded " << ori::describe(folded.clip)
                      << " already there: " << (folded.already ? "yes" : "no") << "\n";
            std::cout << "the stroke belongs to " << ori::owner_of(folded.clip, "s1").value_or("nobody")
                      << "\n";
        }
    }
    sync::session_close(reviewer);
    sync::session_close(host);
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/share-an-annotation/cpp.cpp</sub>

Zig:

```zig
const std = @import("std");
const otio = @import("otio");
const ori = otio.ext.ori;
const sync = otio.ext.sync;

// The cut both sides have: one shot, which sync knows by the GUID
// main/shot.
const 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_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.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.
const welcome =
    \\{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
const hostWelcome =
    \\{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
const anchor =
    \\{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot","sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},"capturedAt":1787742000000}
;

// One stroke: a PaintStart, its points and a PaintEnd sharing a uuid.
const stroke =
    \\[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},{"OTIO_SCHEMA":"PaintPoint.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"}]
;

// 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.
const 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,"revision":1,"createdAt":0,"anchor":{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot","sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},"capturedAt":1787742000000},"bodyMarkdown":"Too dark here.","annotationCommands":[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},{"OTIO_SCHEMA":"PaintPoint.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"}]}}
;

// Stands in for the network: a real program writes every frame the session
// has waiting to its WebSocket to Furnace.
fn send(allocator: std.mem.Allocator, io: std.Io, session: u64) !void {
    const now = std.Io.Clock.real.now(io).toMilliseconds();
    while (try sync.sessionPollTransmit(allocator, session, now)) |frame| {
        std.debug.print("send {d} bytes\n", .{frame.bytes.len});
    }
}

pub fn main(init: std.process.Init) !void {
    const allocator = init.arena.allocator();
    const io = init.io;

    // ORI Annotations is an extension; enabling it enables sync too.
    try ori.enable();

    // The reviewer joins with the right to comment.
    const reviewer = try sync.sessionNew(
        \\{"token":"join-code","identity":{"capabilities":["review.comment.create","review.annotation.create"]}}
    );
    try sync.sessionConnect(reviewer);
    try send(allocator, io, reviewer);
    try sync.sessionReceive(reviewer, welcome, false);
    while (try sync.sessionPollEvent(allocator, reviewer)) |_| {}

    // Commit the stroke with a note. The session sends it again after every
    // reconnect until Furnace stores it, so it is not lost.
    const seen = try otio.Document.fromJson(cut);
    defer seen.deinit();
    const seen_root = (try seen.root()) orelse return error.Empty;
    const id = try ori.commit(seen, allocator, reviewer, seen_root, anchor, "Too dark here.", stroke, "");
    std.debug.print("committed {s}\n", .{id});
    try send(allocator, io, reviewer);

    // The host hears the stored comment and folds it into its timeline.
    const host = try sync.sessionNew(
        \\{"token":"host-token","identity":{"capabilities":["review.commit.receive"]}}
    );
    try sync.sessionConnect(host);
    try send(allocator, io, host);
    try sync.sessionReceive(host, hostWelcome, false);
    try sync.sessionReceive(host, relay, false);
    const document = try otio.Document.fromJson(cut);
    defer document.deinit();
    const timeline = (try document.root()) orelse return error.Empty;
    while (try sync.sessionPollEvent(allocator, host)) |text| {
        const event = try allocator.dupeZ(u8, text);
        // A stroke still being drawn: draw it over the picture, then drop it.
        if (try ori.readPartial(allocator, event)) |partial| {
            std.debug.print("drawing {s}\n", .{partial});
            continue;
        }
        const parsed = try std.json.parseFromSlice(std.json.Value, allocator, event, .{});
        if (!std.mem.eql(u8, parsed.value.object.get("type").?.string, "review_relay")) continue;
        const folded = try ori.fold(document, timeline, event);
        std.debug.print("folded {s} already there: {}\n", .{ try ori.describe(document, allocator, folded.clip), folded.already });
        const owner = try ori.ownerOf(document, allocator, folded.clip, "s1");
        std.debug.print("the stroke belongs to {s}\n", .{owner orelse "nobody"});
    }
    try sync.sessionClose(reviewer);
    try sync.sessionClose(host);
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/share-an-annotation/zig.zig</sub>

C:

```c
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#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;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/share-an-annotation/c.c</sub>

C#:

```csharp
using System;
using System.Text;
using System.Text.Json;
using OpenTimelineIO;
using OpenTimelineIO.Ext.Ori;
using OpenTimelineIO.Ext.Sync;

// The cut both sides have: one shot, which sync knows by the GUID
// main/shot.
const string 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_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.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.
const string Welcome = """{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
const string HostWelcome = """{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"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.
const string Anchor = """{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot","sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},"capturedAt":1787742000000}""";

// One stroke: a PaintStart, its points and a PaintEnd sharing a uuid.
const string Stroke = """[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},{"OTIO_SCHEMA":"PaintPoint.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"}]""";

// 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.
const string 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,"revision":1,"createdAt":0,"anchor":{"sequenceId":"main","capturedGeneration":1,"clipGuid":"main/shot","sourceTime":{"value":10,"rate":24},"capturedSequenceTime":{"value":10,"rate":24},"capturedAt":1787742000000},"bodyMarkdown":"Too dark here.","annotationCommands":[{"OTIO_SCHEMA":"PaintStart.1","uuid":"s1","rgba":[1,0.25,0.25,1]},{"OTIO_SCHEMA":"PaintPoint.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"}]}}""";

// ORI Annotations is an extension; enabling it enables sync too.
OriExtension.Enable();

// The reviewer joins with the right to comment.
var reviewer = SyncExtension.SessionNew(
    """{"token":"join-code","identity":{"capabilities":["review.comment.create","review.annotation.create"]}}""");
SyncExtension.SessionConnect(reviewer);
SendAll(reviewer);
SyncExtension.SessionReceive(reviewer, Encoding.UTF8.GetBytes(Welcome), false);
while (SyncExtension.SessionPollEvent(reviewer) is not null)
{
}

// Commit the stroke with a note. The session sends it again after every
// reconnect until Furnace stores it, so it is not lost.
var seen = Otio.FromJson(Cut);
Console.WriteLine($"committed {OriExtension.Commit(reviewer, seen, Anchor, "Too dark here.", Stroke, "")}");
SendAll(reviewer);

// The host hears the stored comment and folds it into its timeline.
var host = SyncExtension.SessionNew("""{"token":"host-token","identity":{"capabilities":["review.commit.receive"]}}""");
SyncExtension.SessionConnect(host);
SendAll(host);
SyncExtension.SessionReceive(host, Encoding.UTF8.GetBytes(HostWelcome), false);
SyncExtension.SessionReceive(host, Encoding.UTF8.GetBytes(Relay), false);
var timeline = Otio.FromJson(Cut);
while (SyncExtension.SessionPollEvent(host) is string occurrence)
{
    // A stroke still being drawn: draw it over the picture, then drop it.
    if (OriExtension.ReadPartial(occurrence) is string partial)
    {
        Console.WriteLine($"drawing {partial}");
        continue;
    }
    using var parsed = JsonDocument.Parse(occurrence);
    if (parsed.RootElement.GetProperty("type").GetString() != "review_relay")
    {
        continue;
    }
    var (clip, already) = OriExtension.Fold(timeline, occurrence);
    Console.WriteLine($"folded {OriExtension.Describe(clip)} already there: {already}");
    Console.WriteLine($"the stroke belongs to {OriExtension.OwnerOf(clip, "s1") ?? "nobody"}");
}
SyncExtension.SessionClose(reviewer);
SyncExtension.SessionClose(host);

// Stands in for the network: a real program writes every frame the session
// has waiting to its WebSocket to Furnace.
static void SendAll(ulong session)
{
    var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    while (SyncExtension.SessionPollTransmit(session, now) is var (bytes, _))
    {
        Console.WriteLine($"send {bytes.Length} bytes");
    }
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/share-an-annotation/csharp.cs</sub>

Objective-C:

```objectivec
#import <Foundation/Foundation.h>

#import <OpenTimelineIO/OTIOExtOri.h>
#import <OpenTimelineIO/OTIOExtSync.h>

// The cut both sides have: one shot, which sync knows by the GUID
// main/shot.
static NSString *const 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 NSString *const 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 NSString *const 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 NSString *const 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 NSString *const 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 NSString *const 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 TRY(ok)                      \
    if (!(ok)) {                     \
        NSLog(@"%@", error);         \
        return 1;                    \
    }

// Stands in for the network: a real program writes every frame the session
// has waiting to its WebSocket to Furnace.
static BOOL SendAll(uint64_t session, NSError **error) {
    for (;;) {
        NSData *bytes = nil;
        BOOL binary = NO;
        if (!OTIOExtSyncSessionPollTransmit(session, 0, &bytes, &binary, error)) {
            return OTIOIsNoValue(*error);
        }
        printf("send %lu bytes\n", (unsigned long)bytes.length);
    }
}

static BOOL Receive(uint64_t session, NSString *frame, NSError **error) {
    return OTIOExtSyncSessionReceive(session, [frame dataUsingEncoding:NSUTF8StringEncoding], NO,
                                     error);
}

int main(void) {
    @autoreleasepool {
        NSError *error = nil;

        // ORI Annotations is an extension; enabling it enables sync too.
        TRY(OTIOExtOriEnable(&error));

        // The reviewer joins with the right to comment.
        uint64_t reviewer = 0;
        TRY(OTIOExtSyncSessionNew(@"{\"token\":\"join-code\",\"identity\":{\"capabilities\":[\"review.comment.create\",\"review.annotation.create\"]}}",
                                  &reviewer, &error));
        TRY(OTIOExtSyncSessionConnect(reviewer, &error) && SendAll(reviewer, &error));
        TRY(Receive(reviewer, WELCOME, &error));
        while (OTIOExtSyncSessionPollEvent(reviewer, &error) != nil) {
        }

        // Commit the stroke with a note. The session sends it again after
        // every reconnect until Furnace stores it, so it is not lost.
        OTIOSerializableObject *seen = OTIOFromJSON(CUT, &error);
        TRY(seen != nil);
        NSString *mutation =
            OTIOExtOriCommit(reviewer, seen, ANCHOR, @"Too dark here.", STROKE, @"", &error);
        TRY(mutation != nil);
        printf("committed %s\n", mutation.UTF8String);
        TRY(SendAll(reviewer, &error));

        // The host hears the stored comment and folds it into its timeline.
        uint64_t host = 0;
        TRY(OTIOExtSyncSessionNew(@"{\"token\":\"host-token\",\"identity\":{\"capabilities\":[\"review.commit.receive\"]}}", &host,
                                  &error));
        TRY(OTIOExtSyncSessionConnect(host, &error) && SendAll(host, &error));
        TRY(Receive(host, HOST_WELCOME, &error) && Receive(host, RELAY, &error));
        OTIOSerializableObject *timeline = OTIOFromJSON(CUT, &error);
        TRY(timeline != nil);
        for (;;) {
            NSString *event = OTIOExtSyncSessionPollEvent(host, &error);
            if (event == nil) {
                break;
            }
            // A stroke still being drawn: draw it over the picture, then drop it.
            NSString *partial = OTIOExtOriReadPartial(event, &error);
            if (partial != nil) {
                printf("drawing %s\n", partial.UTF8String);
                continue;
            }
            NSDictionary *parsed = [NSJSONSerialization
                JSONObjectWithData:[event dataUsingEncoding:NSUTF8StringEncoding]
                           options:0
                             error:NULL];
            if (![[parsed objectForKey:@"type"] isEqual:@"review_relay"]) {
                continue;
            }
            OTIOSerializableObject *clip = nil;
            BOOL already = NO;
            TRY(OTIOExtOriFold(timeline, event, &clip, &already, &error));
            NSString *about = OTIOExtOriDescribe(clip, &error);
            NSString *owner = OTIOExtOriOwnerOf(clip, @"s1", &error);
            TRY(about != nil && owner != nil);
            printf("folded %s already there: %s\n", about.UTF8String, already ? "yes" : "no");
            printf("the stroke belongs to %s\n", owner.UTF8String);
        }
        TRY(OTIOExtSyncSessionClose(reviewer, &error) && OTIOExtSyncSessionClose(host, &error));
    }
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/share-an-annotation/objectivec.m</sub>

### 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/
