# Realtime Tracks (MoQT)

> Go below the modalities and stream your own live tracks. The relay fans out audio, video, and robot/game frames to every subscriber.

When a modality does not fit, work with raw tracks yourself. You publish
a named *track*. Any number of subscribers receive it live over QUIC. A
robot, an agent, a browser, and a recorder can all consume the same
stream. You do not track who listens. Tracks use
[MoQT](https://datatracker.ietf.org/wg/moq/about/) (Media-over-QUIC
Transport). They fan out through the relay at no extra cost.

You use a `MoqtClient`. Every SDK ships it, with method names in that
language's idiomatic case. You publish and subscribe the same way in
each SDK. The relay handles connect, backoff, and reconnect for you.

## 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`). A track also carries a **capability** string. A capability is
an open-ended routing intent (`"asr"`, `"tts"`, `"ros.telemetry"`,
`"media.passthrough"`). The relay / gateway routes 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.firstbyte.fast", token="…",
                            on_state=lambda s: print("state", s))
```

`connect` returns immediately and dials in the background. If the link
drops, the client **auto-reconnects** with capped exponential backoff.
It **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. The client
queues the calls and replays them after connection. Also, the relay
holds a subscribe for a namespace that is not yet announced. Thus a
robot can subscribe to its command track before any controller connects.

## Publish & subscribe a frame track

Frame tracks carry opaque binary objects with a per-frame priority. They
are the correct fit for robot telemetry and game state. The example
below shows a publisher that streams a track and a subscriber that
receives it. The relay fans the track out.

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

pub = MoqtClient.connect("quic://relay.firstbyte.fast")
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.firstbyte.fast")
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.firstbyte.fast", "", func(int) {})
track, _ := pubc.PublishFrame("robot/turtlebot4-001", "odom", "ros.telemetry", "ros2/cdr", 128)
track.Write(tsUs, cdrBytes, 200)

subc, _ := clutchcall.ConnectMoqt("quic://relay.firstbyte.fast", "", 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.firstbyte.fast", "", |_| {}).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.firstbyte.fast", "", |_| {}).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.firstbyte.fast", "", 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.firstbyte.fast", "", 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.firstbyte.fast");
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.firstbyte.fast");
using var sub = subc.SubscribeFrame("robot/turtlebot4-001", "odom",
    (ts, priority, data) => Handle(priority, data));
```

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

const pubc = await MoqtClient.connect("https://relay.firstbyte.fast/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.firstbyte.fast/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#), the handle owns the native
> callback. If the handle is collected, the engine calls into freed
> memory. The publication handle owns the track. Drop the handle (or call
> `close`) to stop the publication.

## Audio tracks

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

## Namespacing & routing

- **Telemetry vs commands:** give command (inbound) tracks a **distinct
  namespace** from telemetry. For example, publish telemetry under
  `robot/<id>` and subscribe to 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.

## How it works

You get one publish/subscribe API in every SDK, but two runtimes sit
behind it. 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 are held to a common behaviour contract. The
publish/subscribe APIs match, but queue depth, filter handling, and
unsubscribe lifecycle can differ.

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