> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reactor.inc/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Reactor hosts multiple models, each with its own connect slug (modelName) and command/event schema. The catalog of every model — slug, typed SDK package, and links to its schema — is at /model-api-reference/overview. Some models expose one slug per experience (e.g. HappyOyster); always take the slug from the model's own pages, never guess it.
> Fastest path to a working app: `npx create-reactor-app my-app --model=<slug>` scaffolds a complete app with secure auth wired up. Typed TypeScript SDKs are published as @reactor-models/<model>; Python uses the base reactor-sdk package.
> Auth: exchange an API key (rk_...) for a JWT via POST https://api.reactor.inc/tokens from your server. Never put the API key in client-side code.
> Append .md to any docs URL for clean Markdown. Search these docs via the MCP server at https://docs.reactor.inc/mcp.

# Track

> The C++ object form of a named media track

A `reactor::Track` is a handle onto one named media slot the model declared — not something you
construct yourself. Ask for it **by name** with
[`client.track(name)`](/sdk-reference/cpp/reactor#track), or find it by filtering
[`client.tracks()`](#tracklist) when you don't know the name:

```cpp theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto camera = client.track("camera");        // sendonly video
camera.publish().get();
camera.push_frame({bgra.data(), bgra.size()}, 1280, 720);

auto output = client.track("main_video");    // recvonly video
auto frames = output.on_frame([](const reactor::VideoFrame& frame) {
  render(frame.bgra, frame.width, frame.height);
});
```

One type covers both directions and both kinds, because the operations are the same operations
either way. There is no `push_video_frame` / `push_audio_frame` split and no `on_video_frame` /
`on_audio_frame` split at the class level: the track already knows its kind, and the method you call
says which you meant.

<Note>
  A handle, not an owner: it holds the client **weakly**, so a track parked in a capture thread
  cannot keep the session — and the native handle — alive for the life of that thread. Using one
  after the client is gone throws `InvalidStateError`.
</Note>

<Warning>
  Calling a method the track's kind or direction does not allow **throws**, on purpose:
  `push_frame()` on a recvonly track, `on_frame()` on a sendonly one, `pause()` on a sendonly track.
  Each of those would otherwise reach the native layer, find nothing to do, and return — so a caller
  pushing at 30fps would see a model receiving nothing and no reason why.
</Warning>

***

## Properties

### `name()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const std::string& name() const noexcept;
```

The declared name. Never changes.

***

### `kind()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::optional<TrackKind> kind() const;
```

`TrackKind::Video` or `TrackKind::Audio`, or empty before the session has declared its tracks.

***

### `direction()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::optional<TrackDirection> direction() const;
```

`TrackDirection::SendOnly` or `TrackDirection::RecvOnly`, or empty before the session has declared
them. `SendOnly` is from this client's point of view: this client sends, the model receives.

***

### `mid()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::optional<std::string> mid() const;
```

The SDP media id, once the track has been received. Read from the client rather than remembered
here: it is reported as tracks arrive and is renegotiated on a reconnect.

***

### `paused()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
bool paused() const;
```

Whether this track is paused right now. Read from the session, not cached, so it stays right across
a reconnect — recvonly tracks resume automatically once connected, and a `Track` holding a stale
`true` would go on claiming otherwise.

***

### `published()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
bool published() const;
```

Whether this sendonly slot is activated. Kept by the SDK rather than read back, because the session
does not record it: `publish` is a control request and `unpublish` a notification, and neither
leaves anything to query.

<Note>
  **It is cleared whenever the status leaves `Ready`.** A reconnect resumes recvonly tracks and
  nothing else, so a slot published before one is not published after it — publish again. See
  [`reconnect()`](/sdk-reference/cpp/reactor#reconnect).
</Note>

***

## Sending

### `publish()`

Activates this sendonly slot, so the model has something to receive on.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<void> publish();
```

Publishing is what puts a sender behind the slot: pushing before it would drop the frame, and this
SDK [refuses](#push_frame) rather than letting it. Throws on a recvonly track, and on a session that
is not `Ready`.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto camera = client.track("camera");
camera.publish().get();
```

***

### `unpublish()`

Deactivates the slot. Synchronous, unlike the other track methods — there is no round trip, only a
local state change and a fire-and-forget notification.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
void unpublish();
```

Throws when the notification could not be made; the track then stays published, so a retry is
possible.

***

### `push_frame()`

Pushes one BGRA frame into this track.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
void push_frame(Bytes bgra, std::uint32_t width, std::uint32_t height,
                const FrameOptions& options = {});
```

<ParamField path="bgra" type="Bytes" required>
  Exactly `width * height * 4` bytes: B, G, R, A per pixel. Checked here, because the native layer
  reads what it is told to read and a wrong length is a read past the end of your buffer.
</ParamField>

<ParamField path="width" type="std::uint32_t" required>
  Frame width in pixels.
</ParamField>

<ParamField path="height" type="std::uint32_t" required>
  Frame height in pixels.
</ParamField>

<ParamField path="options" type="FrameOptions">
  Metadata and capture time — see below.
</ParamField>

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
camera.push_frame({bgra.data(), bgra.size()}, 1280, 720);
```

Throws `InvalidStateError` on a recvonly track, before [`publish()`](#publish), or once the session
has left `Ready`; `BadRequestError` on a buffer whose length does not match the dimensions.

### `FrameOptions`

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct FrameOptions {
  Bytes user_data;
  std::optional<std::int64_t> capture_time_us;
};
```

<ParamField path="user_data" type="Bytes">
  Bytes the far end reads as this frame's metadata. Sent as-is — JSON, protobuf or anything else is
  between you and the model — and dropped silently by a peer that did not declare it reads them, so
  tagging is safe whatever the far end supports. See [Frame Metadata](/concepts/frame-metadata).
</ParamField>

<ParamField path="capture_time_us" type="std::optional<std::int64_t>">
  When this frame was captured, read from
  [`reactor::time_micros()`](/sdk-reference/cpp/reactor#time_micros). Left empty, the frame is
  stamped as it is pushed, so several tracks capturing one moment arrive microseconds apart.
</ParamField>

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
reactor::Track::FrameOptions options;
options.user_data = {tag.data(), tag.size()};
options.capture_time_us = reactor::time_micros();
camera.push_frame({bgra.data(), bgra.size()}, 1280, 720, options);
```

***

### `push_audio()`

Pushes interleaved 16-bit PCM into this track.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
void push_audio(Samples pcm, std::uint32_t sample_rate = 48'000, std::uint32_t channels = 1);
```

`sample_rate` must be 48000 and `channels` 1, which is what the source expects; `pcm.size` must
divide evenly by `channels`.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
mic_track.push_audio({samples.data(), samples.size()});
```

***

### `set_bitrate()`

Bounds what this one sender may spend, in bits per second.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<void> set_bitrate(Bitrate bounds);
```

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct Bitrate {
  std::optional<std::int32_t> min_bps;
  std::optional<std::int32_t> max_bps;
};
```

**This is the ceiling that actually caps a video encoder**, and it is easy to hit without knowing it
exists: with nothing set, WebRTC derives a sender's maximum from the frame size alone, and that
maximum is 2500 kbps for anything above 960x540. 720p, 1080p and 4K all cap at 2.5 Mbps.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
reactor::Track::Bitrate cap;
cap.max_bps = 8'000'000;
camera.set_bitrate(cap).get();
```

<Note>
  [`Reactor::set_bitrate()`](/sdk-reference/cpp/reactor#set_bitrate) is the other ceiling — the whole
  connection's budget. The two are conjunctive, so raising only one changes nothing.

  A ceiling is permission, not a target: the encoder still spends only what the congestion controller
  allocated and what the picture needs. What raising it buys is headroom for the moments that would
  otherwise clip.
</Note>

Throws on a recvonly track — the sender behind an incoming track is the far end's, and nothing here
can bound it — and on a session that is not `Ready`.

***

## Receiving

### `on_frame()`

Receives decoded video frames from this track.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
Subscription on_frame(std::function<void(const VideoFrame&)> handler);
```

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto frames = output.on_frame([](const reactor::VideoFrame& frame) {
  render(frame.bgra, frame.width, frame.height);
});
```

Only this track's frames reach it — every media handler in this SDK is scoped to one track. Throws
on a sendonly track (the callback would never fire, which is indistinguishable from a model that
sends nothing) and on an audio track, which has [`on_audio()`](#on_audio).

<Warning>
  Hold the returned [`Subscription`](/sdk-reference/cpp/types#subscription). Discarding it
  unregisters the handler immediately.
</Warning>

***

### `on_audio()`

Receives decoded audio frames. Refuses the wrong kind or direction, as `on_frame()` does.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
Subscription on_audio(std::function<void(const AudioFrame&)> handler);
```

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto audio = speech.on_audio([](const reactor::AudioFrame& frame) {
  play(frame.samples, frame.num_samples, frame.sample_rate);
});
```

***

### `VideoFrame`

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct VideoFrame {
  std::string_view track_name;
  const std::uint8_t* bgra;
  std::uint32_t width;
  std::uint32_t height;
  std::uint64_t frame_id;
  std::uint64_t timestamp_us;
  Bytes user_data;

  std::size_t size_bytes() const noexcept;   // width * height * 4
  bool has_metadata() const noexcept;
};
```

<ParamField path="track_name" type="std::string_view">
  The track this arrived on. Every recvonly video track decodes into one callback, so on a session
  with several this is what tells them apart.
</ParamField>

<ParamField path="bgra" type="const std::uint8_t*">
  BGRA pixels: `width * height * 4` bytes, which `size_bytes()` returns.
</ParamField>

<ParamField path="frame_id" type="std::uint64_t">
  The sender's frame counter, or 0 when the frame carried no metadata trailer.
</ParamField>

<ParamField path="timestamp_us" type="std::uint64_t">
  The sender's capture time in microseconds, or 0 with no trailer. Read in the engine's clock
  ([`time_micros()`](/sdk-reference/cpp/reactor#time_micros)), not the system's.
</ParamField>

<ParamField path="user_data" type="Bytes">
  Whatever the sender tagged this frame with — bytes, not text. Empty when the frame carried no
  trailer, which is the normal case for a model that does not tag.
</ParamField>

<Warning>
  **The pixels are borrowed for the duration of the handler and no longer.** The library frees them
  when the handler returns, so anything you keep has to be copied — a pointer stored here is a
  use-after-free that reproduces under load and not in tests. The same goes for `user_data` and
  `track_name`.
</Warning>

<Note>
  The handler runs **inline on the library's delivery thread**, deliberately. Blocking in it is the
  backpressure: while it runs, the library keeps only the newest frame and drops the ones in between.
  Handing frames to a queue of your own trades a bounded drop for unbounded latency and memory.

  This is why media handlers do not go through
  [`Options::executor`](/sdk-reference/cpp/reactor#executor), and control-event handlers do.
</Note>

***

### `AudioFrame`

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct AudioFrame {
  std::string_view track_name;
  const std::int16_t* samples;
  std::uint32_t num_samples;   // total, across all channels
  std::uint32_t sample_rate;
  std::uint32_t channels;

  std::uint32_t frames() const noexcept;   // num_samples / channels
};
```

Borrowed and inline, exactly as `VideoFrame`. The audio queue is short and keeps its backlog rather
than dropping, because there the queue is the jitter buffer and a hole in it is audible — so a slow
handler here costs latency instead of frames.

`frames()` is samples per channel, which is what a playback device asks for.

***

## Pausing

### `pause()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<void> pause();
```

Stops this track. Nothing is generated while paused, which on a video track is visible only as a
frozen frame.

***

### `resume()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<void> resume();
```

Starts it again.

***

## `TrackList`

The tracks a session declared, filterable — for discovery, and for a caller who would rather not
hardcode a name. Returned by [`client.tracks()`](/sdk-reference/cpp/reactor#tracks).

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
TrackList with_kind(TrackKind kind) const;
TrackList with_direction(TrackDirection direction) const;
Track one() const;

bool empty() const noexcept;
std::size_t size() const noexcept;
const Track& operator[](std::size_t index) const;
```

Filters chain in either order, and it iterates:

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto output = client.tracks()
                  .with_direction(reactor::TrackDirection::RecvOnly)
                  .with_kind(reactor::TrackKind::Video)
                  .one();

for (const auto& track : client.tracks()) {
  std::cout << track.name() << '\n';
}
```

`one()` throws `NotFoundError` when the list is empty and `InvalidStateError` when it holds more
than one — a filter that matched several and a caller that wanted one is a question with no answer,
and picking the first would answer it wrongly and silently.

***

## `TrackKind` and `TrackDirection`

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
enum class TrackKind : std::uint8_t { Video, Audio };

enum class TrackDirection : std::uint8_t { SendOnly, RecvOnly };
```

Both convert to and from their wire spellings:

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::string_view to_string(TrackKind kind) noexcept;
std::string_view to_string(TrackDirection direction) noexcept;

std::optional<TrackKind> track_kind_from_string(std::string_view text) noexcept;
std::optional<TrackDirection> track_direction_from_string(std::string_view text) noexcept;
```

The parsers return empty for a value this build does not recognise, rather than guessing.
