# Timeline sync

> Sharing an OpenTimelineIO timeline between programs while it is edited, over Alchemist's Furnace or ORI Sync.

Timeline sync keeps one timeline the same in several programs while people
edit it: an editor in the browser, a reviewer on an iPad, a desktop editor,
RV or xStudio. It is otio-rust's first extension.

Two systems already do this. ORI Sync, from the ORI Annotations project, runs
between OpenRV and xStudio over RabbitMQ. Alchemist's Furnace is a WebSocket
server between Alchemist's clients. Both name every object with a GUID in
`metadata.sync.guid` and send changes as patches addressed by GUID. Timeline
sync is one implementation of that model, written once in Rust and reached
from every language, that speaks both systems' wire formats.

## Where it stands

Sync is being built in phases, tracked in
[epic #115](https://github.com/alchemist-editor/otio-rust/issues/115):

1. **The extension itself**: sync can be enabled, and every SDK has its
   module.
2. **Identity and patches**: GUIDs, patches made by transactions or by
   comparing two versions of a timeline, and applying them.
3. **Sessions over Furnace**: a session that takes bytes in and hands bytes
   out, with no networking of its own, Furnace's wire format, a WebSocket
   client, and compression, in Rust and, through the C interface, in every
   SDK.
4. **Furnace relays patches** instead of whole timelines.
5. **ORI's wire format** over RabbitMQ, for RV and xStudio, in Rust.

## Turning it on

| Language | Import | Enable |
| --- | --- | --- |
| Rust | `otio-ext-sync` | `otio_ext_sync::install(); otio_ext::enable(otio_ext_sync::ID)?;` |
| Python | `from opentimelineio.ext import sync` | `sync.enable()` |
| C | `#include "otio_ext_sync.h"` | `otio_ext_sync_enable(&error);` |
| Go | `github.com/alchemist-editor/otio-rust/sdk/go/ext/sync` | `sync.Enable()` |
| Swift | `import OTIOExtSync` | `try Sync.enable()` |
| TypeScript | `import * as sync from "@alchemist-edit/otio/ext/sync"` | `await init(); sync.enable();` |
| C++ | `opentimelineio/ext/sync.hpp` | `otio::ext::sync::enable();` |
| C# | `using OpenTimelineIO.Ext.Sync;` | `SyncExtension.Enable();` |
| Objective-C | `#import <OpenTimelineIO/OTIOExtSync.h>` | `OTIOExtSyncEnable(&error);` |
| Zig | `otio.ext.sync` | `try otio.ext.sync.enable();` |

## Identity

Every object sync addresses carries a GUID in `metadata.sync.guid`: the
timeline, its stack, and every track, stack and item under it. Markers,
effects and media references travel inside the item that holds them. A GUID
is ordinary metadata, so a file carrying them opens anywhere OpenTimelineIO
does.

Sync gives an object a GUID when it needs one. Beginning a transaction
stamps every object that lacks one, and an object that shares a GUID with
one met earlier (usually a copy made with its metadata) gets a new one; the
first keeps it. You can also stamp a timeline yourself, read or set one
object's GUID, make a new GUID, and find an object by its GUID.

## Patches

A change to a timeline travels as a list of patches, in Alchemist's Furnace
format: `insert_child`, `move_child`, `remove_child` and `set_property`,
each naming the objects it is about by GUID. A change too big for patches
(more than 500 of them, or more than 40% of the timeline's objects), a change
between two sequences, or one that goes back a generation, is sent as one
`replace_timeline` patch holding the whole timeline instead.

There are two ways to get patches, and both give the same ones.

**Compare two versions.** Diffing two snapshots, each a timeline with its
sequence id and generation as Furnace sends it, gives the patches between
them. It is a port of Furnace's own diff, checked against Furnace's golden
files in Rust and in every SDK, so both ends agree byte for byte.

Rust:

```rust
use otio_ext_sync::{Snapshot, diff, patch};

// Two versions of one sequence, as Furnace sends them: in the second, clip B
// has moved in front of clip A. Every object carries its GUID in
// `metadata.sync.guid`, which is how a patch names it.
const BEFORE: &str = r#"{"sequenceId": "main", "generation": 1, "otioJson": {
  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
     "children": [
       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}},
       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}}]}]}}}"#;

const AFTER: &str = r#"{"sequenceId": "main", "generation": 2, "otioJson": {
  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
     "children": [
       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}},
       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}}]}]}}}"#;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    otio_ext_sync::install();
    otio_ext::enable(otio_ext_sync::ID)?;

    let before = Snapshot::from_json(BEFORE)?;
    let after = Snapshot::from_json(AFTER)?;
    let (patches, stats) = diff(&before, &after)?;

    // One move, of A to after B: a patch names objects by GUID.
    println!("{}", patch::to_json_pretty(&patches));
    println!("fell back to the whole timeline: {}", stats.fell_back);
    Ok(())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/diff-two-timelines/rust.rs</sub>

Python:

```python
import json

from opentimelineio.ext import sync

# Two versions of one sequence, as Furnace sends them: in the second, clip B
# has moved in front of clip A. Every object carries its GUID in
# `metadata.sync.guid`, which is how a patch names it.
BEFORE = """{"sequenceId": "main", "generation": 1, "otioJson": {
  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
     "children": [
       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}},
       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}}]}]}}}"""

AFTER = """{"sequenceId": "main", "generation": 2, "otioJson": {
  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
     "children": [
       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}},
       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}}]}]}}}"""

sync.enable()

patches = sync.diff(BEFORE, AFTER, pretty=True)
stats = json.loads(sync.diff_stats(BEFORE, AFTER))

# One move, of A to after B: a patch names objects by GUID.
print(patches)
print("fell back to the whole timeline:", "true" if stats["fellBack"] else "false")
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/diff-two-timelines/python.py</sub>

TypeScript:

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

// Two versions of one sequence, as Furnace sends them: in the second, clip B
// has moved in front of clip A. Every object carries its GUID in
// `metadata.sync.guid`, which is how a patch names it.
const clip = (name: string, guid: string) => ({
  OTIO_SCHEMA: "Clip.2",
  name,
  metadata: { sync: { guid } },
});
const snapshot = (generation: number, children: object[]) =>
  JSON.stringify({
    sequenceId: "main",
    generation,
    otioJson: {
      OTIO_SCHEMA: "Timeline.1",
      name: "cut",
      metadata: { sync: { guid: "cut" } },
      tracks: {
        OTIO_SCHEMA: "Stack.1",
        metadata: { sync: { guid: "tracks" } },
        children: [
          { OTIO_SCHEMA: "Track.1", name: "V1", kind: "Video", metadata: { sync: { guid: "v1" } }, children },
        ],
      },
    },
  });

const before = snapshot(1, [clip("A", "a"), clip("B", "b")]);
const after = snapshot(2, [clip("B", "b"), clip("A", "a")]);

await init();
sync.enable();

// One move, of A to after B: a patch names objects by GUID.
console.log(sync.diff(before, after, true));
const stats = JSON.parse(sync.diffStats(before, after)) as { fellBack: boolean };
console.log(`fell back to the whole timeline: ${stats.fellBack}`);
```

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

Go:

```go
package main

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

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

// Two versions of one sequence, as Furnace sends them: in the second, clip B
// has moved in front of clip A. Every object carries its GUID in
// metadata.sync.guid, which is how a patch names it.
const before = `{"sequenceId": "main", "generation": 1, "otioJson": {
  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
     "children": [
       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}},
       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}}]}]}}}`

const after = `{"sequenceId": "main", "generation": 2, "otioJson": {
  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
     "children": [
       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}},
       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}}]}]}}}`

func main() {
	if err := sync.Enable(); err != nil {
		log.Fatal(err)
	}

	patches, err := sync.Diff(before, after, true)
	if err != nil {
		log.Fatal(err)
	}
	stats, err := sync.DiffStats(before, after)
	if err != nil {
		log.Fatal(err)
	}
	var summary struct {
		FellBack bool `json:"fellBack"`
	}
	if err := json.Unmarshal([]byte(stats), &summary); err != nil {
		log.Fatal(err)
	}

	// One move, of A to after B: a patch names objects by GUID.
	fmt.Println(patches)
	fmt.Println("fell back to the whole timeline:", summary.FellBack)
}
```

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

Swift:

```swift
import Foundation
import OTIOExtSync

// Two versions of one sequence, as Furnace sends them: in the second, clip B
// has moved in front of clip A. Every object carries its GUID in
// `metadata.sync.guid`, which is how a patch names it.
let before = """
    {"sequenceId": "main", "generation": 1, "otioJson": {
      "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
      "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
        {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
         "children": [
           {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}},
           {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}}]}]}}}
    """

let after = """
    {"sequenceId": "main", "generation": 2, "otioJson": {
      "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
      "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
        {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
         "children": [
           {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}},
           {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}}]}]}}}
    """

try Sync.enable()

let patches = try Sync.diff(before, newSnapshot: after, pretty: true)
let stats = try JSONSerialization.jsonObject(
    with: Data(try Sync.diffStats(before, newSnapshot: after).utf8)) as? [String: Any]

// One move, of A to after B: a patch names objects by GUID.
print(patches)
print("fell back to the whole timeline: \(stats?["fellBack"] as? Bool ?? false)")
```

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

C++:

```cpp
#include <iostream>

#include <opentimelineio/ext/sync.hpp>

// Two versions of one sequence, as Furnace sends them: in the second, clip B
// has moved in front of clip A. Every object carries its GUID in
// `metadata.sync.guid`, which is how a patch names it.
const char *const BEFORE = R"({"sequenceId": "main", "generation": 1, "otioJson": {
  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
     "children": [
       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}},
       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}}]}]}}})";

const char *const AFTER = R"({"sequenceId": "main", "generation": 2, "otioJson": {
  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
     "children": [
       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}},
       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}}]}]}}})";

int main() {
    namespace sync = otio::ext::sync;
    sync::enable();

    // One move, of A to after B: a patch names objects by GUID. The stats
    // say whether the change was too large for patches and fell back to
    // the whole timeline.
    std::cout << sync::diff(BEFORE, AFTER, true) << "\n";
    std::cout << sync::diff_stats(BEFORE, AFTER) << "\n";
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/diff-two-timelines/cpp.cpp</sub>

Zig:

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

// Two versions of one sequence, as Furnace sends them: in the second, clip B
// has moved in front of clip A. Every object carries its GUID in
// `metadata.sync.guid`, which is how a patch names it.
const before =
    \\{"sequenceId": "main", "generation": 1, "otioJson": {
    \\  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
    \\  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    \\    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
    \\     "children": [
    \\       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}},
    \\       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}}]}]}}}
;

const after =
    \\{"sequenceId": "main", "generation": 2, "otioJson": {
    \\  "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
    \\  "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
    \\    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
    \\     "children": [
    \\       {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}},
    \\       {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}}]}]}}}
;

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    try sync.enable();

    const patches = try sync.diff(allocator, before, after, true);
    const stats = try std.json.parseFromSliceLeaky(
        struct { fellBack: bool },
        allocator,
        try sync.diffStats(allocator, before, after),
        .{ .ignore_unknown_fields = true },
    );

    // One move, of A to after B: a patch names objects by GUID.
    std.debug.print("{s}\n", .{patches});
    std.debug.print("fell back to the whole timeline: {}\n", .{stats.fellBack});
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/diff-two-timelines/zig.zig</sub>

C:

```c
#include <stdio.h>

#include "otio_ext_sync.h"

/* Two versions of one sequence, as Furnace sends them: in the second, clip B
 * has moved in front of clip A. Every object carries its GUID in
 * metadata.sync.guid, which is how a patch names it. */
#define CLIP(name, guid) \
    "{\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"" name "\"," \
    " \"metadata\": {\"sync\": {\"guid\": \"" guid "\"}}}"
#define SNAPSHOT(generation, first, second) \
    "{\"sequenceId\": \"main\", \"generation\": " generation ", \"otioJson\": {" \
    "\"OTIO_SCHEMA\": \"Timeline.1\", \"name\": \"cut\"," \
    " \"metadata\": {\"sync\": {\"guid\": \"cut\"}}," \
    " \"tracks\": {\"OTIO_SCHEMA\": \"Stack.1\"," \
    " \"metadata\": {\"sync\": {\"guid\": \"tracks\"}}, \"children\": [" \
    "{\"OTIO_SCHEMA\": \"Track.1\", \"name\": \"V1\", \"kind\": \"Video\"," \
    " \"metadata\": {\"sync\": {\"guid\": \"v1\"}}," \
    " \"children\": [" first ", " second "]}]}}}"

static const char BEFORE[] = SNAPSHOT("1", CLIP("A", "a"), CLIP("B", "b"));
static const char AFTER[] = SNAPSHOT("2", CLIP("B", "b"), CLIP("A", "a"));

int main(void) {
    OtioBuffer error = {0}, patches = {0}, stats = {0};

    if (otio_ext_sync_enable(&error) != OTIO_STATUS_OK ||
        otio_ext_sync_diff(BEFORE, AFTER, true, &patches, &error) != OTIO_STATUS_OK ||
        otio_ext_sync_diff_stats(BEFORE, AFTER, &stats, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        return 1;
    }

    /* One move, of A to after B: a patch names objects by GUID. */
    printf("%s\n%s\n", patches.data, stats.data);
    otio_buffer_free(patches);
    otio_buffer_free(stats);
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/diff-two-timelines/c.c</sub>

C#:

```csharp
using System;
using OpenTimelineIO.Ext.Sync;

// Two versions of one sequence, as Furnace sends them: in the second, clip B
// has moved in front of clip A. Every object carries its GUID in
// `metadata.sync.guid`, which is how a patch names it.
const string Before = """
    {"sequenceId": "main", "generation": 1, "otioJson": {
      "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
      "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
        {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
         "children": [
           {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}},
           {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}}]}]}}}
    """;

const string After = """
    {"sequenceId": "main", "generation": 2, "otioJson": {
      "OTIO_SCHEMA": "Timeline.1", "name": "cut", "metadata": {"sync": {"guid": "cut"}},
      "tracks": {"OTIO_SCHEMA": "Stack.1", "metadata": {"sync": {"guid": "tracks"}}, "children": [
        {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "metadata": {"sync": {"guid": "v1"}},
         "children": [
           {"OTIO_SCHEMA": "Clip.2", "name": "B", "metadata": {"sync": {"guid": "b"}}},
           {"OTIO_SCHEMA": "Clip.2", "name": "A", "metadata": {"sync": {"guid": "a"}}}]}]}}}
    """;

SyncExtension.Enable();

// One move, of A to after B: a patch names objects by GUID.
Console.WriteLine(SyncExtension.Diff(Before, After, true));
Console.WriteLine(SyncExtension.DiffStats(Before, After));
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/diff-two-timelines/csharp.cs</sub>

Objective-C:

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

#import <OpenTimelineIO/OTIOExtSync.h>

// Two versions of one sequence, as Furnace sends them: in the second, clip B
// has moved in front of clip A. Every object carries its GUID in
// metadata.sync.guid, which is how a patch names it.
#define CLIP(name, guid) \
    "{\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"" name "\"," \
    " \"metadata\": {\"sync\": {\"guid\": \"" guid "\"}}}"
#define SNAPSHOT(generation, first, second) \
    @"{\"sequenceId\": \"main\", \"generation\": " generation ", \"otioJson\": {" \
    "\"OTIO_SCHEMA\": \"Timeline.1\", \"name\": \"cut\"," \
    " \"metadata\": {\"sync\": {\"guid\": \"cut\"}}," \
    " \"tracks\": {\"OTIO_SCHEMA\": \"Stack.1\"," \
    " \"metadata\": {\"sync\": {\"guid\": \"tracks\"}}, \"children\": [" \
    "{\"OTIO_SCHEMA\": \"Track.1\", \"name\": \"V1\", \"kind\": \"Video\"," \
    " \"metadata\": {\"sync\": {\"guid\": \"v1\"}}," \
    " \"children\": [" first ", " second "]}]}}}"

int main(void) {
    @autoreleasepool {
        NSString *before = SNAPSHOT("1", CLIP("A", "a"), CLIP("B", "b"));
        NSString *after = SNAPSHOT("2", CLIP("B", "b"), CLIP("A", "a"));
        NSError *error = nil;

        if (!OTIOExtSyncEnable(&error)) {
            NSLog(@"%@", error);
            return 1;
        }
        NSString *patches = OTIOExtSyncDiff(before, after, YES, &error);
        NSString *stats = patches == nil ? nil : OTIOExtSyncDiffStats(before, after, &error);
        if (stats == nil) {
            NSLog(@"%@", error);
            return 1;
        }

        // One move, of A to after B: a patch names objects by GUID.
        printf("%s\n%s\n", patches.UTF8String, stats.UTF8String);
    }
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/diff-two-timelines/objectivec.m</sub>

**Record a transaction.** Begin a transaction on a timeline, edit it with
any of the usual calls, and commit it for the patches. Rolling back puts the
timeline back as it was. The transaction remembers the timeline as it began
and diffs it against the timeline as it is, so its patches are exactly what
comparing the two versions gives.

Applying patches to a timeline changes it in place. The timeline keeps its
identity, and so does every track and item the patches keep, so anything
your program holds onto still refers to the same object afterwards. An
object the patches removed is gone, and one they added can be found by its
GUID.

Rust:

```rust
use otio_ext_sync::document::{self, Transaction};

// A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
const CUT: &str = r#"{
  "OTIO_SCHEMA": "Timeline.1",
  "name": "cut",
  "tracks": {"OTIO_SCHEMA": "Stack.1", "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "children": [
      {"OTIO_SCHEMA": "Clip.2", "name": "A"},
      {"OTIO_SCHEMA": "Clip.2", "name": "B"}]}]}
}"#;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    otio_ext_sync::install();
    otio_ext::enable(otio_ext_sync::ID)?;

    // Beginning a transaction gives every object a GUID, and remembers the
    // timeline as it was: that snapshot is what a second program starts
    // from.
    let mut editor = otio_core::from_str(CUT)?;
    let timeline = editor.root().ok_or("the cut has no root")?;
    let transaction = Transaction::begin(&mut editor, timeline, "main", 1)?;
    let mut viewer = otio_core::from_str(&transaction.before().timeline)?;
    let shown = viewer.root().ok_or("the snapshot has no root")?;

    // Edit as usual, then commit for the patches that say what changed.
    if let Some(base) = editor.try_get_mut(timeline)?.base_mut() {
        base.name = "fine cut".into();
    }
    let patches = transaction.commit(&mut editor)?;
    println!("{}", otio_ext_sync::patch::to_json(&patches));

    // The other copy applies them in place: what it held still names the
    // same objects.
    document::apply_to(&mut viewer, shown, &patches)?;
    let name = viewer.try_get(shown)?.base().map(|base| base.name.clone());
    println!("the viewer now shows {}", name.unwrap_or_default());
    Ok(())
}
```

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

Python:

```python
import opentimelineio as otio
from opentimelineio.ext import sync

# A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
CUT = """{
  "OTIO_SCHEMA": "Timeline.1",
  "name": "cut",
  "tracks": {"OTIO_SCHEMA": "Stack.1", "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "children": [
      {"OTIO_SCHEMA": "Clip.2", "name": "A"},
      {"OTIO_SCHEMA": "Clip.2", "name": "B"}]}]}
}"""

sync.enable()

# Beginning a transaction gives every object a GUID, and returns the
# timeline as it was as a snapshot: that snapshot is the transaction. A
# second program starts from the same stamped timeline.
timeline = otio.adapters.read_from_string(CUT, "otio_json")
before = sync.begin(timeline, "main", 1)
shown = otio.adapters.read_from_string(timeline.to_json_string(), "otio_json")

# Edit as usual, then commit for the patches that say what changed.
timeline.name = "fine cut"
patches = sync.commit(timeline, before)
print(patches)

# The other copy applies them in place: what it held is still the same
# object.
sync.apply_to(shown, patches)
print("the viewer now shows", shown.name)
```

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

TypeScript:

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

// A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
const CUT = `{
  "OTIO_SCHEMA": "Timeline.1",
  "name": "cut",
  "tracks": {"OTIO_SCHEMA": "Stack.1", "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "children": [
      {"OTIO_SCHEMA": "Clip.2", "name": "A"},
      {"OTIO_SCHEMA": "Clip.2", "name": "B"}]}]}
}`;

const asTimeline = (json: string): Timeline => {
  const node = deserializeJsonFromString(json);
  if (!(node instanceof Timeline)) {
    throw new Error("not a timeline");
  }
  return node;
};

await init();
sync.enable();

// Beginning a transaction gives every object a GUID, and remembers the
// timeline as it was: that snapshot is the transaction. A second program
// starts from the same stamped timeline.
const editor = asTimeline(CUT);
const before = sync.begin(editor, "main", 1);
const viewer = asTimeline(serializeJsonToString(editor));

// Edit as usual, then commit for the patches that say what changed.
editor.name = "fine cut";
const patches = sync.commit(editor, before);
console.log(patches);

// The other copy applies them in place: what it held still names the
// same objects.
sync.applyTo(viewer, patches);
console.log(`the viewer now shows ${viewer.name}`);
```

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

Go:

```go
package main

import (
	"fmt"
	"log"

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

// A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
const cut = `{
  "OTIO_SCHEMA": "Timeline.1",
  "name": "cut",
  "tracks": {"OTIO_SCHEMA": "Stack.1", "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "children": [
      {"OTIO_SCHEMA": "Clip.2", "name": "A"},
      {"OTIO_SCHEMA": "Clip.2", "name": "B"}]}]}
}`

func main() {
	if err := sync.Enable(); err != nil {
		log.Fatal(err)
	}

	// Beginning a transaction gives every object a GUID, and remembers the
	// timeline as it was: that snapshot is what a second program starts
	// from.
	editor, err := otio.FromJSON(cut)
	if err != nil {
		log.Fatal(err)
	}
	defer editor.Close()
	before, err := sync.Begin(editor, "main", 1)
	if err != nil {
		log.Fatal(err)
	}
	stamped, err := editor.ToJSON(0)
	if err != nil {
		log.Fatal(err)
	}
	viewer, err := otio.FromJSON(stamped)
	if err != nil {
		log.Fatal(err)
	}
	defer viewer.Close()

	// Edit as usual, then commit for the patches that say what changed.
	if err := editor.SetName("fine cut"); err != nil {
		log.Fatal(err)
	}
	patches, err := sync.Commit(editor, before)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(patches)

	// The other copy applies them in place: what it held still names the
	// same objects.
	if err := sync.ApplyTo(viewer, patches); err != nil {
		log.Fatal(err)
	}
	name, err := viewer.Name()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("the viewer now shows", name)
}
```

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

Swift:

```swift
import OTIOExtSync
import OpenTimelineIO

// A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
let cut = """
    {
      "OTIO_SCHEMA": "Timeline.1",
      "name": "cut",
      "tracks": {"OTIO_SCHEMA": "Stack.1", "children": [
        {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "children": [
          {"OTIO_SCHEMA": "Clip.2", "name": "A"},
          {"OTIO_SCHEMA": "Clip.2", "name": "B"}]}]}
    }
    """

try Sync.enable()

// Beginning a transaction gives every object a GUID, and remembers the
// timeline as it was: that snapshot is the transaction. A second program
// starts from the same stamped timeline.
let editor = try OTIO.fromJSON(cut)
let before = try Sync.begin(editor, sequenceID: "main", generation: 1)
let viewer = try OTIO.fromJSON(try editor.toJSON(0))

// Edit as usual, then commit for the patches that say what changed.
try editor.setName("fine cut")
let patches = try Sync.commit(editor, before: before)
print(patches)

// The other copy applies them in place: what it held still names the same
// objects.
try Sync.applyTo(viewer, patches: patches)
print("the viewer now shows \(try viewer.name())")
```

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

C++:

```cpp
#include <iostream>

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

// A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
const char *const CUT = R"({
  "OTIO_SCHEMA": "Timeline.1",
  "name": "cut",
  "tracks": {"OTIO_SCHEMA": "Stack.1", "children": [
    {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "children": [
      {"OTIO_SCHEMA": "Clip.2", "name": "A"},
      {"OTIO_SCHEMA": "Clip.2", "name": "B"}]}]}
})";

int main() {
    namespace sync = otio::ext::sync;
    sync::enable();

    // Beginning a transaction gives every object a GUID, and returns the
    // timeline as it was as a snapshot: that snapshot is the transaction.
    // A second program starts from the same stamped timeline.
    otio::Timeline editor = otio::from_json(CUT).as<otio::Timeline>().value();
    const std::string before = sync::begin(editor, "main", 1);
    const otio::Timeline viewer = otio::from_json(editor.to_json(0)).as<otio::Timeline>().value();

    // Edit as usual, then commit for the patches that say what changed.
    editor.set_name("fine cut");
    const std::string patches = sync::commit(editor, before);
    std::cout << patches << "\n";

    // The other copy applies them in place: what it held still names the
    // same objects.
    sync::apply_to(viewer, patches);
    std::cout << "the viewer now shows " << viewer.name() << "\n";
    return 0;
}
```

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

Zig:

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

// A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
const cut =
    \\{"OTIO_SCHEMA": "Timeline.1", "name": "cut",
    \\ "tracks": {"OTIO_SCHEMA": "Stack.1", "children": [
    \\  {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "children": [
    \\   {"OTIO_SCHEMA": "Clip.2", "name": "A"},
    \\   {"OTIO_SCHEMA": "Clip.2", "name": "B"}]}]}}
;

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    try sync.enable();

    // Beginning a transaction gives every object a GUID, and writes the
    // timeline as it was as a snapshot: that snapshot is the transaction.
    // A second program starts from the same stamped timeline.
    const editor = try otio.Document.fromJson(cut);
    defer editor.deinit();
    const timeline = (try editor.root()) orelse return error.Empty;
    const before = try sync.begin(editor, allocator, timeline, "main", 1);
    const json = try editor.toJson(allocator, 0);
    const viewer = try otio.Document.fromJson(try allocator.dupeZ(u8, json));
    defer viewer.deinit();
    const shown = (try viewer.root()) orelse return error.Empty;

    // Edit as usual, then commit for the patches that say what changed.
    try timeline.setName("fine cut");
    const patches = try sync.commit(editor, allocator, timeline, try allocator.dupeZ(u8, before));
    std.debug.print("{s}\n", .{patches});

    // The other copy applies them in place: what it held still names the
    // same objects.
    try sync.applyTo(viewer, shown, try allocator.dupeZ(u8, patches));
    std.debug.print("the viewer now shows {s}\n", .{try shown.name(allocator)});
}
```

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

C:

```c
#include <stdio.h>

#include "otio_ext_sync.h"

/* A cut, as an .otio file would hold it. Nothing in it has a GUID yet. */
static const char CUT[] =
    "{\"OTIO_SCHEMA\": \"Timeline.1\", \"name\": \"cut\","
    " \"tracks\": {\"OTIO_SCHEMA\": \"Stack.1\", \"children\": ["
    "{\"OTIO_SCHEMA\": \"Track.1\", \"name\": \"V1\", \"kind\": \"Video\", \"children\": ["
    "{\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"A\"},"
    " {\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"B\"}]}]}}";

int main(void) {
    OtioBuffer error = {0}, before = {0}, json = {0}, patches = {0}, name = {0};
    OtioDocument *editor = NULL, *viewer = NULL;
    OtioNode timeline = otio_node_none(), shown = otio_node_none();
    int failed = 1;

    if (otio_ext_sync_enable(&error) != OTIO_STATUS_OK ||
        otio_document_from_json(CUT, &editor, &error) != OTIO_STATUS_OK ||
        otio_document_root(editor, &timeline, &error) != OTIO_STATUS_OK) {
        goto done;
    }

    /* Beginning a transaction gives every object a GUID, and writes the
     * timeline as it was as a snapshot: that snapshot is the transaction.
     * A second program starts from the same stamped timeline. */
    if (otio_ext_sync_begin(editor, timeline, "main", 1, &before, &error) != OTIO_STATUS_OK ||
        otio_document_to_json(editor, 0, &json, &error) != OTIO_STATUS_OK ||
        otio_document_from_json(json.data, &viewer, &error) != OTIO_STATUS_OK ||
        otio_document_root(viewer, &shown, &error) != OTIO_STATUS_OK) {
        goto done;
    }

    /* Edit as usual, then commit for the patches that say what changed. */
    if (otio_node_set_name(editor, timeline, "fine cut", &error) != OTIO_STATUS_OK ||
        otio_ext_sync_commit(editor, timeline, before.data, &patches, &error) != OTIO_STATUS_OK) {
        goto done;
    }
    printf("%s\n", patches.data);

    /* The other copy applies them in place: what it held still names the
     * same objects. */
    if (otio_ext_sync_apply_to(viewer, shown, patches.data, &error) != OTIO_STATUS_OK ||
        otio_node_name(viewer, shown, &name, &error) != OTIO_STATUS_OK) {
        goto done;
    }
    printf("the viewer now shows %s\n", name.data);
    failed = 0;

done:
    if (failed) {
        fprintf(stderr, "%s\n", error.data);
    }
    otio_buffer_free(error);
    otio_buffer_free(before);
    otio_buffer_free(json);
    otio_buffer_free(patches);
    otio_buffer_free(name);
    otio_document_free(editor);
    otio_document_free(viewer);
    return failed;
}
```

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

C#:

```csharp
using System;
using OpenTimelineIO;
using OpenTimelineIO.Ext.Sync;

// A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
const string Cut = """
    {
      "OTIO_SCHEMA": "Timeline.1",
      "name": "cut",
      "tracks": {"OTIO_SCHEMA": "Stack.1", "children": [
        {"OTIO_SCHEMA": "Track.1", "name": "V1", "kind": "Video", "children": [
          {"OTIO_SCHEMA": "Clip.2", "name": "A"},
          {"OTIO_SCHEMA": "Clip.2", "name": "B"}]}]}
    }
    """;

SyncExtension.Enable();

// Beginning a transaction gives every object a GUID, and returns the
// timeline as it was as a snapshot: that snapshot is the transaction. A
// second program starts from the same stamped timeline.
var timeline = Otio.FromJson(Cut);
var before = SyncExtension.Begin(timeline, "main", 1);
var shown = Otio.FromJson(timeline.ToJson(0));

// Edit as usual, then commit for the patches that say what changed.
timeline.SetName("fine cut");
var patches = SyncExtension.Commit(timeline, before);
Console.WriteLine(patches);

// The other copy applies them in place: what it held still names the same
// objects.
SyncExtension.ApplyTo(shown, patches);
Console.WriteLine($"the viewer now shows {shown.Name()}");
```

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

Objective-C:

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

#import <OpenTimelineIO/OTIOExtSync.h>

// A cut, as an .otio file would hold it. Nothing in it has a GUID yet.
static NSString *const CUT =
    @"{\"OTIO_SCHEMA\": \"Timeline.1\", \"name\": \"cut\","
     " \"tracks\": {\"OTIO_SCHEMA\": \"Stack.1\", \"children\": ["
     "{\"OTIO_SCHEMA\": \"Track.1\", \"name\": \"V1\", \"kind\": \"Video\", \"children\": ["
     "{\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"A\"},"
     " {\"OTIO_SCHEMA\": \"Clip.2\", \"name\": \"B\"}]}]}}";

int main(void) {
    @autoreleasepool {
        NSError *error = nil;
        if (!OTIOExtSyncEnable(&error)) {
            NSLog(@"%@", error);
            return 1;
        }
        OTIOSerializableObject *timeline = OTIOFromJSON(CUT, &error);
        if (timeline == nil) {
            NSLog(@"%@", error);
            return 1;
        }

        // Beginning a transaction gives every object a GUID, and writes the
        // timeline as it was as a snapshot: that snapshot is the transaction.
        // A second program starts from the same stamped timeline.
        NSString *before = OTIOExtSyncBegin(timeline, @"main", 1, &error);
        NSString *json = before == nil ? nil : [timeline toJSON:0 error:&error];
        OTIOSerializableObject *shown = json == nil ? nil : OTIOFromJSON(json, &error);
        if (shown == nil) {
            NSLog(@"%@", error);
            return 1;
        }

        // Edit as usual, then commit for the patches that say what changed.
        NSString *patches = [timeline setName:@"fine cut" error:&error]
                                ? OTIOExtSyncCommit(timeline, before, &error)
                                : nil;
        if (patches == nil) {
            NSLog(@"%@", error);
            return 1;
        }
        printf("%s\n", patches.UTF8String);

        // The other copy applies them in place: what it held still names the
        // same objects.
        NSString *name = OTIOExtSyncApplyTo(shown, patches, &error) ? [shown name:&error] : nil;
        if (name == nil) {
            NSLog(@"%@", error);
            return 1;
        }
        printf("the viewer now shows %s\n", name.UTF8String);
    }
    return 0;
}
```

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

## Noticing changes

A program that edits a timeline through code it does not own, such as an
editing operation or a script, can turn on the document's change journal.
The journal records which objects any edit touched. Committing it runs the
`document_changed` hook, with the GUIDs of the touched objects as its
`touched` argument, so a hook script can send the change on. The journal is
off unless you turn it on, and costs nothing while it is off.

The design is in
[ADR 0011](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0011-timeline-sync.md).

## Sessions

A session joins a shared timeline and keeps a copy of it in step. It does no
networking itself: a program hands it what arrived and the session hands back
what to send and what happened. The `otio-ext-sync-transport` crate puts it on
a wire. Its first wire format is Furnace's protocol v2, and it has a blocking
WebSocket client that runs the whole loop:

```rust
use std::time::Duration;
use otio_ext_sync::session::{self, ApplyOptions, Config, Event, Session};
use otio_ext_sync_transport::websocket::Client;

let config = Config { token: join_token, ..Config::default() };
let mut client = Client::connect(&ws_url, Session::new(config))?;
while let Some(event) = client.next_event(Duration::from_secs(1))? {
    // Applies timeline changes to `timeline` in place and runs the hooks.
    session::handle(&mut document, timeline, &event, &ApplyOptions::default())?;
}
```

The host publishes with `session.publish(&mut document, timeline, "main")`,
which works out the patches since the last publish and sends them. Viewers
receive each change as patches and apply them in place, so handles to the
timeline, its tracks and its items keep working. A host that reconnects to a
Furnace holding an older generation than its own, because the connection
dropped as an edit went out, sends its timeline again.

### Patches on the wire

When Furnace's welcome lists `timeline.patch`, the host sends each change as
a `patch` message holding the patches and the generations they go from and
to. Furnace applies them to its copy, so anyone who joins later still gets
the whole timeline in their welcome, and relays them to every client whose
hello listed `timeline.patch.receive` (sessions list it unless told not to).
Everyone else, older clients included, gets the timeline Furnace rebuilt, as
before. The host still sends the whole timeline for its first publish, every
64th change after that as a checkpoint, when the patches would be bigger
than the timeline, when the change replaces the timeline outright, and to a
Furnace that does not list `timeline.patch`.

If Furnace's copy is not the generation a patch starts from, or the patch
does not apply to it, Furnace answers `stale_patch` or `patch_rejected`, and
the host sends its whole timeline in reply. A viewer that receives patches
from a generation it does not have is out of sync: `receive` fails with an
out-of-sync error (a `ConnectionError` in Python, `IoError` over the C
interface), and the program reconnects to get the whole timeline in the
welcome. Furnace's conformance corpus, revision 5, holds a patch and the
timeline it makes, and Go and Rust must both reach the same timeline.

Everything a session sends travels on a channel with a delivery rule:

| Channel | Rule | Over Furnace |
| --- | --- | --- |
| timeline | in order | `patch`, or `snapshot`, from the host only |
| playback | newest wins: a playhead not yet sent is replaced | `presence` |
| presence | newest wins | `welcome` and `roster` |
| review | kept until acknowledged, refused or cancelled, sent again after a reconnect | `review_commit` |
| signal | sent once, never replayed | `signal` |

Only the host may change the timeline, and only a client with the
`review.comment.create` capability may commit a review comment. A session
refuses these itself rather than letting Furnace close the connection. For
the same reason `session.send` carries only Furnace's message types that have
no call of their own (`signal`, and from the host only `bin` and the LAN
messages), and review
comments are refused when an older Furnace chooses protocol v1, which has no
review ledger.
Selections and other extensions' channels need Furnace to relay them, which
it does not do yet.

Four hooks run around a session: `sync_before_send` before the host's
changes leave (a script that fails stops them), `sync_before_apply` before a
remote change is applied (a script that fails rejects it), `sync_after_apply`
after, and `sync_joined` once the join is done. The media linker runs on the
clips a remote change inserts.

### From other languages

Every SDK has the same session, over the C interface. The program owns the
socket: open a WebSocket to Furnace with whatever your language offers,
connect the session, and then keep three things going. Send every frame the
session gives you (text frames as text, binary ones as binary), hand it every
frame that arrives, and act on each event it reports. Passing a `timeline` or
`joined` event to `handle` with your copy of the timeline applies it in
place and runs the hooks, as `session::handle` does in Rust.

A session is a number from `session_new`, released with `session_close`.
Its settings, its events and its state are JSON objects, in the field names
Furnace uses:

| | Fields |
| --- | --- |
| Settings | `token`, `identity` (`displayName`, `app`, `appVersion`, `credential`, `deviceId`, `clientKind`, `formFactor`, `deviceName`, `capabilities`), `encodings` (`br`, `gzip`); all optional |
| Event | `type`: `joined`, `timeline`, `timeline_removed`, `playhead`, `selection`, `roster`, `leader`, `review_relay`, `review_committed`, `review_rejected`, `error`, `ended` or `message`, and that event's fields |
| State | `joined`, `ended`, `sessionId`, `selfId`, `role`, `leader`, `peers`, `activeSequence`, `sequences`, `pendingReviews`, `version`, `encodings` |

Python wraps this in a `Session` class that takes and returns dictionaries,
and `poll_transmit` there returns `str` for a text frame and `bytes` for a
binary one.

Rust:

```rust
use otio_ext_sync::session::{self, ApplyOptions, Config, Event, Playhead, Session};
use otio_ext_sync_transport::{Connection, Frame, Furnace, now_millis};

// The frame Furnace sends a viewer joining a session whose host has shared
// one timeline: the welcome, holding the timeline as it is.
const WELCOME: &str = r#"{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","role":"viewer","roster":[{"id":"p_1","displayName":"Host","role":"host","joinedAt":0,"connectionState":"connected","muted":false}],"session":{"id":"s_1","createdAt":0},"snapshot":{"sequenceId":"main","generation":1,"otioJson":{"OTIO_SCHEMA":"Timeline.1","name":"Coastline","metadata":{"sync":{"guid":"t-1"}},"tracks":{"OTIO_SCHEMA":"Stack.1","metadata":{"sync":{"guid":"s-1"}},"children":[{"OTIO_SCHEMA":"Track.1","name":"V1","kind":"Video","metadata":{"sync":{"guid":"v-1"}},"children":[{"OTIO_SCHEMA":"Clip.2","name":"Waves","metadata":{"sync":{"guid":"c-1"}}}]}]}}}}}"#;

// A real program writes each frame to its WebSocket to Furnace, or lets
// `otio_ext_sync_transport::websocket::Client` do it all.
fn send(connection: &mut Connection) -> Result<(), otio_ext_sync_transport::Error> {
    while let Some(frame) = connection.poll_transmit(now_millis())? {
        let kind = if frame.binary { "binary" } else { "text" };
        println!("send a {kind} frame of {} bytes", frame.data.len());
    }
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    otio_ext_sync::install();
    otio_ext::enable(otio_ext_sync::ID)?;

    // The session owns no socket. Open one to Furnace, then connect: the
    // hello is the first frame to send.
    let mut config = Config {
        token: "join-code".to_string(),
        ..Config::default()
    };
    config.identity.display_name = "Ada".to_string();
    let mut connection = Connection::new(Session::new(config), Furnace::new());
    connection.connect();
    send(&mut connection)?;

    // Hand the session every frame that arrives. This is the one Furnace
    // sends back.
    connection.receive(&Frame::text(WELCOME))?;

    // The program's copy of the timeline starts empty. Handling the events
    // puts the host's timeline in it, and keeps it current from then on.
    let mut document = otio_core::from_str(r#"{"OTIO_SCHEMA": "Timeline.1"}"#)?;
    let timeline = document.root().ok_or("the document has no root")?;
    while let Some(event) = connection.poll_event() {
        let handled = session::handle(&mut document, timeline, &event, &ApplyOptions::default())?;
        let kind = match event {
            Event::Timeline(_) => "timeline",
            Event::Joined { .. } => "joined",
            _ => "other",
        };
        println!("{kind} {handled:?}");
    }
    println!(
        "the viewer now shows {}",
        document.try_get(timeline)?.name()
    );

    // Everyone in the session sees where this viewer's playhead is.
    connection.session_mut().set_playhead(Playhead {
        sequence_id: "main".to_string(),
        frame: 24.0,
        playing: true,
        rate: 1.0,
    })?;
    send(&mut connection)?;

    connection.session_mut().leave();
    send(&mut connection)?;
    Ok(())
}
```

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

Python:

```python
import opentimelineio as otio
from opentimelineio.ext import sync

# The frame Furnace sends a viewer joining a session whose host has shared
# one timeline: the welcome, holding the timeline as it is.
WELCOME = (
    '{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{'
    '"selfId":"p_2","role":"viewer","roster":[{"id":"p_1","displayName":"Host","role":"host",'
    '"joinedAt":0,"connectionState":"connected","muted":false}],"session":{"id":"s_1","createdAt":0},'
    '"snapshot":{"sequenceId":"main","generation":1,"otioJson":{"OTIO_SCHEMA":"Timeline.1",'
    '"name":"Coastline","metadata":{"sync":{"guid":"t-1"}},"tracks":{"OTIO_SCHEMA":"Stack.1",'
    '"metadata":{"sync":{"guid":"s-1"}},"children":[{"OTIO_SCHEMA":"Track.1","name":"V1",'
    '"kind":"Video","metadata":{"sync":{"guid":"v-1"}},"children":[{"OTIO_SCHEMA":"Clip.2",'
    '"name":"Waves","metadata":{"sync":{"guid":"c-1"}}}]}]}}}}}'
)


def send(session):
    # A real program writes each frame to its WebSocket to Furnace: text
    # frames as text, binary ones as binary.
    while (frame := session.poll_transmit()) is not None:
        kind = "text" if isinstance(frame, str) else "binary"
        print(f"send a {kind} frame of {len(frame)} characters or bytes")


sync.enable()

# The session owns no socket. Open one to Furnace, then connect: the hello
# is the first frame to send.
session = sync.Session(token="join-code", identity={"displayName": "Ada"})
session.connect()
send(session)

# Hand the session every frame that arrives. This is the one Furnace sends
# back.
session.receive(WELCOME)

# The program's copy of the timeline starts empty. Handling the events puts
# the host's timeline in it, and keeps it current from then on.
timeline = otio.schema.Timeline()
while (event := session.poll_event()) is not None:
    print(event["type"], sync.handle(timeline, event)["handled"])
print("the viewer now shows", timeline.name)

# Everyone in the session sees where this viewer's playhead is.
session.set_playhead("main", 24, playing=True)
send(session)

session.leave()
send(session)
```

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

TypeScript:

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

// What Furnace sends a viewer joining a session whose host shared one
// timeline.
const WELCOME =
  '{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","role":"viewer","roster":[{"id":"p_1","displayName":"Host","role":"host","joinedAt":0,"connectionState":"connected","muted":false}],"session":{"id":"s_1","createdAt":0},"snapshot":{"sequenceId":"main","generation":1,"otioJson":{"OTIO_SCHEMA":"Timeline.1","name":"Coastline","metadata":{"sync":{"guid":"t-1"}},"tracks":{"OTIO_SCHEMA":"Stack.1","metadata":{"sync":{"guid":"s-1"}},"children":[{"OTIO_SCHEMA":"Track.1","name":"V1","kind":"Video","metadata":{"sync":{"guid":"v-1"}},"children":[{"OTIO_SCHEMA":"Clip.2","name":"Waves","metadata":{"sync":{"guid":"c-1"}}}]}]}}}}}';

await init();
sync.enable();

// A session owns no socket. A real program writes each frame to its
// WebSocket to Furnace; this one only says what it would send.
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 as ${frame.binary ? "binary" : "text"}`);
  }
};

const session = sync.sessionNew('{"token":"join-code","identity":{"displayName":"Ada"}}');
sync.sessionConnect(session);
send(session);

// The frame Furnace sends back.
sync.sessionReceive(session, new TextEncoder().encode(WELCOME), false);

// The program's own copy of the timeline, empty until the host's arrives.
const timeline = deserializeJsonFromString('{"OTIO_SCHEMA": "Timeline.1"}');
if (!(timeline instanceof Timeline)) {
  throw new Error("not a timeline");
}
for (let event = sync.sessionPollEvent(session); event !== undefined; event = sync.sessionPollEvent(session)) {
  const { type } = JSON.parse(event) as { type: string };
  console.log(`${type}: ${sync.handle(timeline, event)}`);
}
console.log(`joined, watching ${timeline.name}`);

// Moving the playhead is one more frame to send.
sync.sessionSetPlayhead(session, "main", 24, true, 1);
send(session);

// Say goodbye before closing the socket.
sync.sessionLeave(session);
send(session);
sync.sessionClose(session);
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/join-a-session/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/sync"
)

// The frame Furnace sends back to a viewer joining a session whose host
// shares one timeline.
const welcome = `{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","role":"viewer","roster":[{"id":"p_1","displayName":"Host","role":"host","joinedAt":0,"connectionState":"connected","muted":false}],"session":{"id":"s_1","createdAt":0},"snapshot":{"sequenceId":"main","generation":1,"otioJson":{"OTIO_SCHEMA":"Timeline.1","name":"Coastline","metadata":{"sync":{"guid":"t-1"}},"tracks":{"OTIO_SCHEMA":"Stack.1","metadata":{"sync":{"guid":"s-1"}},"children":[{"OTIO_SCHEMA":"Track.1","name":"V1","kind":"Video","metadata":{"sync":{"guid":"v-1"}},"children":[{"OTIO_SCHEMA":"Clip.2","name":"Waves","metadata":{"sync":{"guid":"c-1"}}}]}]}}}}}`

// send stands in for the network. A real program writes the frame to its
// WebSocket to Furnace, as a binary or a text message.
func send(frame []byte, binary bool) {
	kind := "text"
	if binary {
		kind = "binary"
	}
	fmt.Printf("send %d bytes (%s)\n", len(frame), kind)
}

// sendAll sends every frame the session has waiting.
func sendAll(session uint64) {
	for {
		frame, binary, err := sync.SessionPollTransmit(session, time.Now().UnixMilli())
		if errors.Is(err, otio.ErrNoValue) {
			return
		}
		if err != nil {
			log.Fatal(err)
		}
		send(frame, binary)
	}
}

func main() {
	if err := sync.Enable(); err != nil {
		log.Fatal(err)
	}

	// The session owns no socket. Connect once the program's own is open.
	session, err := sync.SessionNew(`{"token":"join-code","identity":{"displayName":"Ada"}}`)
	if err != nil {
		log.Fatal(err)
	}
	if err := sync.SessionConnect(session); err != nil {
		log.Fatal(err)
	}
	sendAll(session)

	// This is the frame Furnace sends back.
	if err := sync.SessionReceive(session, []byte(welcome), false); err != nil {
		log.Fatal(err)
	}

	// The program's copy of the timeline starts empty; the events fill it.
	timeline, err := otio.FromJSON(`{"OTIO_SCHEMA":"Timeline.1"}`)
	if err != nil {
		log.Fatal(err)
	}
	defer timeline.Close()
	for {
		event, err := sync.SessionPollEvent(session)
		if errors.Is(err, otio.ErrNoValue) {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		var kind struct {
			Type string `json:"type"`
		}
		if err := json.Unmarshal([]byte(event), &kind); err != nil {
			log.Fatal(err)
		}
		handled, err := sync.Handle(timeline, event)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(kind.Type, handled)
	}
	name, err := timeline.Name()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("the timeline arrived:", name)

	// Moving the playhead tells everyone else where this viewer is.
	if err := sync.SessionSetPlayhead(session, "main", 24, true, 1); err != nil {
		log.Fatal(err)
	}
	sendAll(session)

	// Leaving queues a goodbye; send it before closing the session.
	if err := sync.SessionLeave(session); err != nil {
		log.Fatal(err)
	}
	sendAll(session)
	if err := sync.SessionClose(session); err != nil {
		log.Fatal(err)
	}
}
```

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

Swift:

```swift
import Foundation
import OTIOExtSync
import OpenTimelineIO

// What Furnace sends a viewer joining a session whose host shared one
// timeline.
let welcome = """
    {"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","role":"viewer","roster":[{"id":"p_1","displayName":"Host","role":"host","joinedAt":0,"connectionState":"connected","muted":false}],"session":{"id":"s_1","createdAt":0},"snapshot":{"sequenceId":"main","generation":1,"otioJson":{"OTIO_SCHEMA":"Timeline.1","name":"Coastline","metadata":{"sync":{"guid":"t-1"}},"tracks":{"OTIO_SCHEMA":"Stack.1","metadata":{"sync":{"guid":"s-1"}},"children":[{"OTIO_SCHEMA":"Track.1","name":"V1","kind":"Video","metadata":{"sync":{"guid":"v-1"}},"children":[{"OTIO_SCHEMA":"Clip.2","name":"Waves","metadata":{"sync":{"guid":"c-1"}}}]}]}}}}}
    """

// A real program writes each frame to its WebSocket to Furnace. This one
// only says what it would send.
func sendFrames(_ session: UInt64) throws {
    while let frame = try Sync.sessionPollTransmit(session, now: 0) {
        print("send \(frame.bytes.count) bytes, \(frame.binary ? "binary" : "text")")
    }
}

try Sync.enable()

// A session owns no socket: it says what to send, and is told what arrived.
let session = try Sync.sessionNew(
    #"{"token":"join-code","identity":{"displayName":"Ada"},"encodings":[]}"#)
try Sync.sessionConnect(session)
try sendFrames(session)

// The frame Furnace sends back.
try Sync.sessionReceive(session, data: Array(welcome.utf8), binary: false)

// The program's copy of the timeline starts empty; handling the events
// fills it in.
let timeline = try OTIO.fromJSON(#"{"OTIO_SCHEMA":"Timeline.1"}"#)
while let event = try Sync.sessionPollEvent(session) {
    let fields = try JSONSerialization.jsonObject(with: Data(event.utf8)) as? [String: Any]
    print("event: \(fields?["type"] ?? "?")")
    print("handled: \(try Sync.handle(timeline, event: event))")
}
print("the timeline is \(try timeline.name())")

// Moving the playhead is one more frame to send.
try Sync.sessionSetPlayhead(session, sequenceID: "main", frame: 24, playing: true, rate: 1)
try sendFrames(session)

// Leaving sends a goodbye. Closing the session then frees it.
try Sync.sessionLeave(session)
try sendFrames(session)
try Sync.sessionClose(session)
```

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

C++:

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

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

namespace sync = otio::ext::sync;

// What Furnace sends a viewer joining a session whose host shared one
// timeline.
const char *const WELCOME = R"({"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","role":"viewer","roster":[{"id":"p_1","displayName":"Host","role":"host","joinedAt":0,"connectionState":"connected","muted":false}],"session":{"id":"s_1","createdAt":0},"snapshot":{"sequenceId":"main","generation":1,"otioJson":{"OTIO_SCHEMA":"Timeline.1","name":"Coastline","metadata":{"sync":{"guid":"t-1"}},"tracks":{"OTIO_SCHEMA":"Stack.1","metadata":{"sync":{"guid":"s-1"}},"children":[{"OTIO_SCHEMA":"Track.1","name":"V1","kind":"Video","metadata":{"sync":{"guid":"v-1"}},"children":[{"OTIO_SCHEMA":"Clip.2","name":"Waves","metadata":{"sync":{"guid":"c-1"}}}]}]}}}}})";

// Sends every frame the session has waiting. A real program writes each one
// to its WebSocket to Furnace; this one only says what it would send.
void send_frames(std::uint64_t session) {
    while (const auto frame = sync::session_poll_transmit(session, 0)) {
        std::cout << "send " << frame->bytes.size() << " bytes as "
                  << (frame->binary ? "binary" : "text") << "\n";
    }
}

// An event's type. A real program reads the event with its JSON library.
std::string type_of(const std::string &event) {
    const std::string key = R"("type":")";
    const std::size_t start = event.find(key) + key.size();
    return event.substr(start, event.find('"', start) - start);
}

int main() {
    sync::enable();

    // A session owns no socket. Connect once yours is open: the hello is
    // the first frame to send.
    const std::uint64_t session =
        sync::session_new(R"({"token":"join-code","identity":{"displayName":"Ada"}})");
    sync::session_connect(session);
    send_frames(session);

    // The frame Furnace sends back.
    const std::string welcome = WELCOME;
    sync::session_receive(session, std::vector<std::uint8_t>(welcome.begin(), welcome.end()), false);

    // The program's own copy of the timeline starts empty. Each event goes
    // to handle, which applies what it can and says what it did.
    const otio::Timeline timeline =
        otio::from_json(R"({"OTIO_SCHEMA":"Timeline.1"})").as<otio::Timeline>().value();
    while (const auto event = sync::session_poll_event(session)) {
        std::cout << "event " << type_of(*event) << ": " << sync::handle(timeline, *event) << "\n";
    }
    std::cout << "joined, showing " << timeline.name() << "\n";

    // Move the playhead, and send what that says to everyone else.
    sync::session_set_playhead(session, "main", 24, true, 1);
    send_frames(session);

    // Leaving queues a goodbye to send before the socket closes. Closing
    // the session afterwards releases it.
    sync::session_leave(session);
    send_frames(session);
    sync::session_close(session);
    return 0;
}
```

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

Zig:

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

// The frame Furnace sends back to a viewer joining a session whose host
// shared one timeline.
const welcome =
    \\{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","role":"viewer","roster":[{"id":"p_1","displayName":"Host","role":"host","joinedAt":0,"connectionState":"connected","muted":false}],"session":{"id":"s_1","createdAt":0},"snapshot":{"sequenceId":"main","generation":1,"otioJson":{"OTIO_SCHEMA":"Timeline.1","name":"Coastline","metadata":{"sync":{"guid":"t-1"}},"tracks":{"OTIO_SCHEMA":"Stack.1","metadata":{"sync":{"guid":"s-1"}},"children":[{"OTIO_SCHEMA":"Track.1","name":"V1","kind":"Video","metadata":{"sync":{"guid":"v-1"}},"children":[{"OTIO_SCHEMA":"Clip.2","name":"Waves","metadata":{"sync":{"guid":"c-1"}}}]}]}}}}}
;

// Sends every frame the session has. A real program writes each one to its
// WebSocket to Furnace; this one only says what it would send.
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| {
        const kind = if (frame.binary) "binary" else "text";
        std.debug.print("send a {s} frame of {d} bytes\n", .{ kind, frame.bytes.len });
    }
}

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

    try sync.enable();

    // A session owns no socket. Connect once the socket is open, and the
    // hello is the first frame to send.
    const session = try sync.sessionNew(
        \\{"token":"join-code","identity":{"displayName":"Ada"}}
    );
    try sync.sessionConnect(session);
    try send(allocator, io, session);

    // Hand the session each frame that arrives.
    try sync.sessionReceive(session, welcome, false);

    // The program's own copy of the timeline starts empty, and each event
    // goes to handle with it.
    const document = try otio.Document.fromJson(
        \\{"OTIO_SCHEMA":"Timeline.1"}
    );
    defer document.deinit();
    const timeline = (try document.root()) orelse return error.Empty;
    while (try sync.sessionPollEvent(allocator, session)) |event| {
        const parsed = try std.json.parseFromSlice(std.json.Value, allocator, event, .{});
        const kind = parsed.value.object.get("type").?.string;
        const handled = try sync.handle(document, allocator, timeline, try allocator.dupeZ(u8, event));
        std.debug.print("{s}: {s}\n", .{ kind, handled });
    }
    std.debug.print("the timeline is {s}\n", .{try timeline.name(allocator)});

    // Move the playhead, and send what that queued.
    try sync.sessionSetPlayhead(session, "main", 24, true, 1);
    try send(allocator, io, session);

    // Leaving queues the goodbye. Close the session once it is sent.
    try sync.sessionLeave(session);
    try send(allocator, io, session);
    try sync.sessionClose(session);
}
```

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

C:

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

#include "otio_ext_sync.h"

/* What Furnace sends a viewer joining a session whose host shared one
 * timeline. */
static const char WELCOME[] =
    "{\"v\":2,\"type\":\"welcome\",\"sessionId\":\"s_1\",\"seq\":0,\"ts\":0,\"order\":1,"
    "\"payload\":{\"selfId\":\"p_2\",\"role\":\"viewer\","
    "\"roster\":[{\"id\":\"p_1\",\"displayName\":\"Host\",\"role\":\"host\",\"joinedAt\":0,"
    "\"connectionState\":\"connected\",\"muted\":false}],"
    "\"session\":{\"id\":\"s_1\",\"createdAt\":0},"
    "\"snapshot\":{\"sequenceId\":\"main\",\"generation\":1,"
    "\"otioJson\":{\"OTIO_SCHEMA\":\"Timeline.1\",\"name\":\"Coastline\","
    "\"metadata\":{\"sync\":{\"guid\":\"t-1\"}},"
    "\"tracks\":{\"OTIO_SCHEMA\":\"Stack.1\",\"metadata\":{\"sync\":{\"guid\":\"s-1\"}},"
    "\"children\":[{\"OTIO_SCHEMA\":\"Track.1\",\"name\":\"V1\",\"kind\":\"Video\","
    "\"metadata\":{\"sync\":{\"guid\":\"v-1\"}},"
    "\"children\":[{\"OTIO_SCHEMA\":\"Clip.2\",\"name\":\"Waves\","
    "\"metadata\":{\"sync\":{\"guid\":\"c-1\"}}}]}]}}}}}";

/* Sends every frame the session has waiting. A real program writes each one
 * to its WebSocket to Furnace; this one only says what it would send. */
static OtioStatus send_frames(uint64_t session, OtioBuffer *error) {
    for (;;) {
        OtioBuffer frame = {0};
        bool binary = false;
        OtioStatus status = otio_ext_sync_session_poll_transmit(session, 0, &frame, &binary, error);
        if (status == OTIO_STATUS_NO_VALUE) {
            /* Nothing left to send. The library still wrote a message to
             * error, which this program owns. */
            otio_buffer_free(*error);
            *error = (OtioBuffer){0};
            return OTIO_STATUS_OK;
        }
        if (status != OTIO_STATUS_OK) {
            return status;
        }
        printf("send %zu bytes as %s\n", frame.len, binary ? "binary" : "text");
        otio_buffer_free(frame);
    }
}

/* Prints an event's type. A real program reads the event with its JSON
 * library. */
static void print_type(const char *event) {
    const char *type = strstr(event, "\"type\":\"");
    if (type != NULL) {
        type += strlen("\"type\":\"");
        printf("event %.*s: ", (int)strcspn(type, "\""), type);
    }
}

int main(void) {
    OtioBuffer error = {0}, name = {0};
    OtioDocument *copy = NULL;
    OtioNode timeline = otio_node_none();
    uint64_t session = 0;
    int failed = 1;

    /* A session owns no socket. Connect once yours is open: the hello is
     * the first frame to send. */
    if (otio_ext_sync_enable(&error) != OTIO_STATUS_OK ||
        otio_ext_sync_session_new("{\"token\": \"join-code\", \"identity\": {\"displayName\": \"Ada\"}}",
                                  &session, &error) != OTIO_STATUS_OK ||
        otio_ext_sync_session_connect(session, &error) != OTIO_STATUS_OK ||
        send_frames(session, &error) != OTIO_STATUS_OK) {
        goto done;
    }

    /* The frame Furnace sends back. */
    if (otio_ext_sync_session_receive(session, (const uint8_t *)WELCOME, strlen(WELCOME), false,
                                      &error) != OTIO_STATUS_OK) {
        goto done;
    }

    /* The program's own copy of the timeline starts empty. Each event goes
     * to otio_ext_sync_handle, which applies what it can and says what it
     * did. */
    if (otio_document_from_json("{\"OTIO_SCHEMA\": \"Timeline.1\"}", &copy, &error) != OTIO_STATUS_OK ||
        otio_document_root(copy, &timeline, &error) != OTIO_STATUS_OK) {
        goto done;
    }
    for (;;) {
        OtioBuffer event = {0}, handled = {0};
        OtioStatus status = otio_ext_sync_session_poll_event(session, &event, &error);
        if (status == OTIO_STATUS_NO_VALUE) {
            otio_buffer_free(error);
            error = (OtioBuffer){0};
            break;
        }
        if (status == OTIO_STATUS_OK) {
            print_type(event.data);
            status = otio_ext_sync_handle(copy, timeline, event.data, &handled, &error);
        }
        if (status == OTIO_STATUS_OK) {
            printf("%s\n", handled.data);
        }
        otio_buffer_free(event);
        otio_buffer_free(handled);
        if (status != OTIO_STATUS_OK) {
            goto done;
        }
    }
    if (otio_node_name(copy, timeline, &name, &error) != OTIO_STATUS_OK) {
        goto done;
    }
    printf("joined, showing %s\n", name.data);

    /* Move the playhead, and send what that says to everyone else. */
    if (otio_ext_sync_session_set_playhead(session, "main", 24, true, 1, &error) != OTIO_STATUS_OK ||
        send_frames(session, &error) != OTIO_STATUS_OK) {
        goto done;
    }

    /* Leaving queues a goodbye to send before the socket closes. Closing
     * the session at the end releases it. */
    if (otio_ext_sync_session_leave(session, &error) != OTIO_STATUS_OK ||
        send_frames(session, &error) != OTIO_STATUS_OK) {
        goto done;
    }
    failed = 0;

done:
    if (failed) {
        fprintf(stderr, "%s\n", error.data);
    }
    otio_ext_sync_session_close(session, NULL);
    otio_buffer_free(error);
    otio_buffer_free(name);
    otio_document_free(copy);
    return failed;
}
```

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

C#:

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

// The frame Furnace sends back to a viewer joining a session whose host
// shared one timeline.
const string Welcome = """{"v":2,"type":"welcome","sessionId":"s_1","seq":0,"ts":0,"order":1,"payload":{"selfId":"p_2","role":"viewer","roster":[{"id":"p_1","displayName":"Host","role":"host","joinedAt":0,"connectionState":"connected","muted":false}],"session":{"id":"s_1","createdAt":0},"snapshot":{"sequenceId":"main","generation":1,"otioJson":{"OTIO_SCHEMA":"Timeline.1","name":"Coastline","metadata":{"sync":{"guid":"t-1"}},"tracks":{"OTIO_SCHEMA":"Stack.1","metadata":{"sync":{"guid":"s-1"}},"children":[{"OTIO_SCHEMA":"Track.1","name":"V1","kind":"Video","metadata":{"sync":{"guid":"v-1"}},"children":[{"OTIO_SCHEMA":"Clip.2","name":"Waves","metadata":{"sync":{"guid":"c-1"}}}]}]}}}}}""";

SyncExtension.Enable();

// A session owns no socket. Make one with the join code, then connect it
// once the socket is open.
var session = SyncExtension.SessionNew("""{"token":"join-code","identity":{"displayName":"Ada"}}""");
SyncExtension.SessionConnect(session);
SendAll(session);

// Hand the session every frame that arrives. This is the one Furnace sends
// back.
SyncExtension.SessionReceive(session, Encoding.UTF8.GetBytes(Welcome), false);

// The program's copy of the timeline starts empty. Handling the events puts
// the host's timeline in it, and keeps it current from then on.
var timeline = Otio.FromJson("""{"OTIO_SCHEMA": "Timeline.1"}""");
while (SyncExtension.SessionPollEvent(session) is string occurrence)
{
    using var parsed = JsonDocument.Parse(occurrence);
    var handled = SyncExtension.Handle(timeline, occurrence);
    Console.WriteLine($"{parsed.RootElement.GetProperty("type")} {handled}");
}
Console.WriteLine($"the viewer now shows {timeline.Name()}");

// Moving the playhead tells everyone else where it is.
SyncExtension.SessionSetPlayhead(session, "main", 24, true, 1);
SendAll(session);

// Leaving queues a goodbye; send it, then the socket can close.
SyncExtension.SessionLeave(session);
SendAll(session);
SyncExtension.SessionClose(session);

// Sends every frame the session has. A real program writes each one to its
// WebSocket to Furnace; this one only says what it would send.
static void SendAll(ulong session)
{
    var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    while (SyncExtension.SessionPollTransmit(session, now) is var (bytes, binary))
    {
        Console.WriteLine($"send {bytes.Length} bytes as {(binary ? "binary" : "text")}");
    }
}
```

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

Objective-C:

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

#import <OpenTimelineIO/OTIOExtSync.h>

// The frame Furnace sends back once the hello arrives: the viewer is in, and
// the host's timeline comes with it.
static NSString *const WELCOME =
    @"{\"v\":2,\"type\":\"welcome\",\"sessionId\":\"s_1\",\"seq\":0,\"ts\":0,\"order\":1,"
     "\"payload\":{\"selfId\":\"p_2\",\"role\":\"viewer\",\"roster\":[{\"id\":\"p_1\","
     "\"displayName\":\"Host\",\"role\":\"host\",\"joinedAt\":0,\"connectionState\":\"connected\","
     "\"muted\":false}],\"session\":{\"id\":\"s_1\",\"createdAt\":0},\"snapshot\":{"
     "\"sequenceId\":\"main\",\"generation\":1,\"otioJson\":{\"OTIO_SCHEMA\":\"Timeline.1\","
     "\"name\":\"Coastline\",\"metadata\":{\"sync\":{\"guid\":\"t-1\"}},\"tracks\":{"
     "\"OTIO_SCHEMA\":\"Stack.1\",\"metadata\":{\"sync\":{\"guid\":\"s-1\"}},\"children\":[{"
     "\"OTIO_SCHEMA\":\"Track.1\",\"name\":\"V1\",\"kind\":\"Video\",\"metadata\":{\"sync\":{"
     "\"guid\":\"v-1\"}},\"children\":[{\"OTIO_SCHEMA\":\"Clip.2\",\"name\":\"Waves\","
     "\"metadata\":{\"sync\":{\"guid\":\"c-1\"}}}]}]}}}}}";

// A real program writes the frame to its WebSocket to Furnace.
static void Send(NSData *bytes, BOOL binary) {
    printf("send %lu bytes (%s)\n", (unsigned long)bytes.length, binary ? "binary" : "text");
}

// Sends every frame the session has waiting.
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);
        }
        Send(bytes, binary);
    }
}

int main(void) {
    @autoreleasepool {
        NSError *error = nil;
        uint64_t session = 0;
        if (!OTIOExtSyncEnable(&error)
            || !OTIOExtSyncSessionNew(
                @"{\"token\":\"join-code\",\"identity\":{\"displayName\":\"Ada\"}}", &session,
                &error)) {
            NSLog(@"%@", error);
            return 1;
        }

        // Connect once the socket is open, and send the hello.
        if (!OTIOExtSyncSessionConnect(session, &error) || !SendAll(session, &error)) {
            NSLog(@"%@", error);
            return 1;
        }

        // Hand the session each frame that arrives.
        NSData *welcome = [WELCOME dataUsingEncoding:NSUTF8StringEncoding];
        if (!OTIOExtSyncSessionReceive(session, welcome, NO, &error)) {
            NSLog(@"%@", error);
            return 1;
        }

        // The program's copy of the timeline starts empty; handle fills it in.
        OTIOSerializableObject *timeline = OTIOFromJSON(@"{\"OTIO_SCHEMA\": \"Timeline.1\"}", &error);
        if (timeline == nil) {
            NSLog(@"%@", error);
            return 1;
        }
        for (;;) {
            NSString *event = OTIOExtSyncSessionPollEvent(session, &error);
            if (event == nil) {
                break;
            }
            NSDictionary *parsed = [NSJSONSerialization
                JSONObjectWithData:[event dataUsingEncoding:NSUTF8StringEncoding]
                           options:0
                             error:NULL];
            NSString *handled = OTIOExtSyncHandle(timeline, event, &error);
            if (handled == nil) {
                NSLog(@"%@", error);
                return 1;
            }
            printf("%s: %s\n", [[parsed objectForKey:@"type"] UTF8String], handled.UTF8String);
        }
        if (!OTIOIsNoValue(error)) {
            NSLog(@"%@", error);
            return 1;
        }
        error = nil;
        NSString *name = [timeline name:&error];
        if (name == nil) {
            NSLog(@"%@", error);
            return 1;
        }
        printf("the host is showing %s\n", name.UTF8String);

        // Move the playhead; the others see it from the frames this sends.
        if (!OTIOExtSyncSessionSetPlayhead(session, @"main", 24, YES, 1, &error)
            || !SendAll(session, &error)) {
            NSLog(@"%@", error);
            return 1;
        }

        // Say goodbye before closing the socket.
        if (!OTIOExtSyncSessionLeave(session, &error) || !SendAll(session, &error)) {
            NSLog(@"%@", error);
            return 1;
        }

        // Then let the session go.
        if (!OTIOExtSyncSessionClose(session, &error)) {
            NSLog(@"%@", error);
            return 1;
        }
    }
    return 0;
}
```

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

### Compression

Each side lists the encodings it accepts when it joins: the client in its
hello, the server in its welcome. Messages of 1 KiB or more go as Brotli or
gzip when the other side accepts it, in a binary frame whose first byte names
the encoding (1 for gzip, 2 for Brotli); everything else stays JSON text.
Furnace does not list any encodings yet, so today every frame is text; the
in-memory test room does, and CI publishes a table of what each encoding
saves on the repository's timelines. A 103 KiB timeline goes over the wire
as about 3 KiB.

### Checked against Furnace

Furnace's conformance corpus, every frame it says a client or server sends,
is vendored into the crate's tests: each client frame is read and written
again and must match it value for value, and each server frame is read as what it
says. A CI job also builds furnaced and has a host and a viewer join a real
session.


## Sessions over ORI Sync

The ORI profile speaks ORI Sync's wire, so a session can join the same
RabbitMQ session as OpenRV and xStudio running ORI's plugins. It is in Rust
for now. There is no server: every peer binds a queue to the session's
fanout exchange, `sync_session_<id>`, and hears everything. The first peer
finds no master within two seconds and elects itself; later peers ask the
master for its state and join with it, and if the master stops answering
another peer takes over.

```rust
use std::time::Duration;
use otio_ext_sync::session::{self, ApplyOptions, Config, Session};
use otio_ext_sync_transport::{amqp::Client, ori};

let options = ori::Options::new("review-42");
let mut client = Client::connect("amqp://guest:guest@localhost:5672/", Session::new(Config::default()), options)?;
loop {
    // `None` is a quiet second, not the end: keep going so the client can
    // elect itself, answer joiners and send its heartbeat.
    if let Some(event) = client.next_event(Duration::from_secs(1))? {
        session::handle(&mut document, timeline, &event, &ApplyOptions::default())?;
    }
}
```

Any peer may change the timelines, as in ORI. What ORI's peers send arrives
as patches like Furnace's, and what a session publishes goes out as ORI's
own edits (`SET_PROPERTY`, `INSERT_CHILD`, `MOVE_CHILD`, `REMOVE_CHILD`),
or as the whole timeline when ORI's edits cannot say it, such as inserting a
track with clips in it. A changed annotation clip goes out as ORI's
`REPLACE_ANNOTATION_COMMANDS`; see
[ORI Annotations](https://otio-rust-docs.vercel.app/docs/extensions/ori-annotations/#on-ori-s-wire). Several timelines, which one is active, the
playhead and ORI's display settings cross too. Selections are retired in
ORI, and review comments, leaders and muting have no ORI message, so sending
them fails as unsupported.

Snapshots and messages over 1 MB can be zlib-compressed, but ORI's own peers
cannot read either yet, so both are off unless `Options` turns them on.
Compressed input is always read.

### Checked against ORI

ORI's own recordings of real sessions are vendored into the crate's tests
and replayed as ORI's recording player replays them; the session must end
with the same timelines, active timeline, playhead and display settings as
ORI's own peer, which CI checks against ORI itself. CI also runs sessions on
a real RabbitMQ, between Rust peers and with ORI's Python peer, with each
side as master.

---

Page: https://otio-rust-docs.vercel.app/docs/extensions/timeline-sync/
