# Realtime Tracks (MoQT)

> Publish and subscribe live media and data over QUIC — audio, video, robot/game frames — fanned out by the relay.

Beyond the request/response RPC surface, ClutchCall exposes a **realtime
track** API: a publisher streams a named *track* into the relay, and any number
of subscribers receive it live over QUIC. Tracks ride
[MoQT](https://datatracker.ietf.org/wg/moq/about/) (Media-over-QUIC Transport)
and are fanned out by the ClutchCall relay mesh, so a robot, an agent, a
browser, and a recorder can all consume the same stream without the publisher
knowing they exist.

Every SDK ships a `MoqtClient` with method names in that language's idiomatic
case. The C++, Python, and Go SDKs share one engine via the
`clutchcall_moqt_ffi` native library; the TypeScript SDK is a standalone
WebTransport implementation with its own queueing, late-join, and reconnect
behaviour. Treat the two families as separate conformance surfaces — a result on
one does not automatically carry to the other until they're held to a common
behaviour contract (publish/subscribe APIs match; queue depth, filter handling,
and unsubscribe lifecycle can differ).

## Track kinds

| Kind      | Use it for                                  | Per-object key |
| --------- | ------------------------------------------- | -------------- |
| **Audio** | Continuous voice (Opus / PCM / G.711).      | rollover ~1 s  |
| **Video** | Encoded video; a group per keyframe (GOP).  | keyframe       |
| **Frame** | Robot telemetry, LIDAR, game state — opaque binary with a **per-frame priority**. | per group |
| **Text**  | Reliable ordered messages (chat, control).  | per 64 msgs    |

A track is addressed by a **namespace** + **name** (e.g. `robot/turtlebot4-001`
+ `odom`) and carries a **capability** string — an open-ended routing intent
(`"asr"`, `"tts"`, `"ros.telemetry"`, `"media.passthrough"`) that the relay /
gateway uses to route the track to whatever registered that capability. You
route on *intent*, not on media kind.

## Connect

```python
from clutchcall.moqt import MoqtClient

client = MoqtClient.connect("quic://relay.clutchcall.dev", token="…",
                            on_state=lambda s: print("state", s))
```

`connect` returns immediately and dials in the background. The client
**auto-reconnects** with capped exponential backoff if the link drops, and
**re-establishes every publication and subscription** on each reconnect — your
code does nothing. `on_state` reports the lifecycle:

| State        | Meaning                                  |
| ------------ | ---------------------------------------- |
| Connecting   | dialling the relay                       |
| Connected    | session up; tracks (re)attached          |
| Reconnecting | link dropped; retrying with backoff      |
| Closed       | you called `close()`                     |
| Failed       | unrecoverable (e.g. bad URL)             |

You may publish or subscribe **before** the session is up — calls are queued
and replayed once connected (and the relay holds a subscribe for a namespace
that has not been announced yet, so a robot can subscribe to its command track
before any controller has connected).

## Publish & subscribe a frame track

Frame tracks carry opaque binary objects with a per-frame priority — the right
fit for robot telemetry and game state. Below: a publisher streaming a track
and a subscriber receiving it, fanned out through the relay.

**Python:**
```python
from clutchcall.moqt import MoqtClient

pub = MoqtClient.connect("quic://relay.clutchcall.dev")
track = pub.publish_frame("robot/turtlebot4-001", "odom",
                          capability="ros.telemetry", schema_tag="ros2/cdr")
track.write(ts_us, cdr_bytes, priority=200)

sub = MoqtClient.connect("quic://relay.clutchcall.dev")
sub.subscribe_frame("robot/turtlebot4-001", "odom",
                    lambda ts, priority, data: handle(priority, data))
```

**Go:**
```go
import clutchcall "github.com/clutchcall/clutchcall-sdk/go/pkg"

pubc, _ := clutchcall.ConnectMoqt("quic://relay.clutchcall.dev", "", func(int) {})
track, _ := pubc.PublishFrame("robot/turtlebot4-001", "odom", "ros.telemetry", "ros2/cdr", 128)
track.Write(tsUs, cdrBytes, 200)

subc, _ := clutchcall.ConnectMoqt("quic://relay.clutchcall.dev", "", func(int) {})
subc.SubscribeFrame("robot/turtlebot4-001", "odom", func(ts uint64, priority uint8, data []byte) {
    handle(priority, data)
})
```

**Rust:**
```rust
use clutchcall_sdk::moqt::MoqtClient;

let pubc = MoqtClient::connect("quic://relay.clutchcall.dev", "", |_| {}).unwrap();
let track = pubc.publish_frame("robot/turtlebot4-001", "odom", "ros.telemetry", "ros2/cdr", 128).unwrap();
track.write(ts_us, &cdr_bytes, 200);

let subc = MoqtClient::connect("quic://relay.clutchcall.dev", "", |_| {}).unwrap();
let _sub = subc.subscribe_frame("robot/turtlebot4-001", "odom",
    |ts, priority, data| handle(priority, data));
```

**Java:**
```java
import com.clutchcall.sdk.Moqt;

Moqt.Client pubc = Moqt.Client.connect("quic://relay.clutchcall.dev", "", st -> {});
Moqt.FramePublication track = pubc.publishFrame("robot/turtlebot4-001", "odom", "ros.telemetry", "ros2/cdr", 128);
track.write(tsUs, cdrBytes, 200);

Moqt.Client subc = Moqt.Client.connect("quic://relay.clutchcall.dev", "", st -> {});
subc.subscribeFrame("robot/turtlebot4-001", "odom",
    (ts, priority, data) -> handle(priority, data));
```

**C#:**
```csharp
using ClutchCall.SDK;

using var pubc = MoqtClient.Connect("quic://relay.clutchcall.dev");
using var track = pubc.PublishFrame("robot/turtlebot4-001", "odom", "ros.telemetry", "ros2/cdr", 128);
track.Write(tsUs, cdrBytes, 200);

using var subc = MoqtClient.Connect("quic://relay.clutchcall.dev");
using var sub = subc.SubscribeFrame("robot/turtlebot4-001", "odom",
    (ts, priority, data) => Handle(priority, data));
```

**TypeScript (browser):**
```ts
import { MoqtClient } from "@clutchcall/sdk/moqt";

const pubc = await MoqtClient.connect("https://relay.clutchcall.dev/moq");
const track = pubc.publishFrame("robot/turtlebot4-001", "odom",
    { capability: "ros.telemetry", schemaTag: "ros2/cdr", defaultPriority: 128 });
track.write(tsUs, cdrBytes, 200);

const subc = await MoqtClient.connect("https://relay.clutchcall.dev/moq");
subc.subscribeFrame("robot/turtlebot4-001", "odom",
    (ts, priority, data) => handle(priority, data));
```

> **NOTE:**
> Keep the subscription handle alive for as long as you want frames. In
> garbage-collected languages (Python/Java/C#) it owns the native callback; if it
> is collected, the engine calls into freed memory. The publication handle owns
> the track — drop it (or call `close`) to stop publishing.

## Audio tracks

Audio tracks are the same shape with codec metadata instead of a priority —
`publish_audio(ns, name, capability, sample_rate, channels, frame_ms)` and
`subscribe_audio(ns, name, on_frame)`, where `on_frame(ts_us, bytes)` delivers
one decoded object. Use them for live voice between an SDK client and an agent;
the `capability` ("asr", "tts", …) routes the track to the right module.

## Namespacing & routing

- **Telemetry vs commands:** give command (inbound) tracks a **distinct
  namespace** from telemetry — e.g. publish telemetry under `robot/<id>` and
  subscribe commands under `robot/<id>/ctl`. If both share `robot/<id>`, the
  relay can route a command subscription to the robot's own telemetry announce.
- **Capability is the routing key.** Two publishers may use different
  capabilities on the same namespace; subscribers and modules select by intent.
- **Fan-out is free.** The relay copies each object to every subscriber; the
  publisher opens one stream per group regardless of subscriber count.

See the per-language **SDK reference** for the full method list, and
[Architecture](/concepts/architecture) for how the relay mesh fans tracks out.
