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

# Track

> The Python object form of a named media track

A `Track` is a handle onto one named media slot the model declared — not something you construct
yourself. Obtain one from [`reactor.track(name)`](/sdk-reference/python/reactor#track) when you know
its name, or by filtering [`reactor.tracks`](/sdk-reference/python/reactor#tracks) — a
[`TrackList`](/sdk-reference/python/types#tracklist) — when you don't:

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
output = reactor.tracks.with_direction("recvonly").with_kind("video").one()
```

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
camera = await reactor.track("camera").publish()   # sendonly video
camera.push_frame(rgb_array)

output = reactor.track("output")       # recvonly video
@output.on_frame
def render(frame): ...
await output.pause()
```

One type covers both directions and both kinds — video and audio — because the operations are the
same operations either way: `push_frame()` sends, `on_frame()` receives, `pause()`/`resume()`
control a `recvonly` track. There is no `push_video_frame` / `push_audio_frame` split at the `Track`
level, and no `on_video_frame` / `on_audio_frame` split: the track already knows its kind.

<Note>
  Push a frame, receive one, or pause/resume a `recvonly` track through this object.
  [`publish_track()`](/sdk-reference/python/reactor#publish_track) /
  [`unpublish_track()`](/sdk-reference/python/reactor#unpublish_track) also stay on `Reactor`
  directly — `publish_track()` hands back this same `Track`. See
  [`Reactor.track()`](/sdk-reference/python/reactor#track) for how to obtain one.
</Note>

<Warning>
  Calling a method the track's direction doesn't allow raises, on purpose — `push_frame()` on a
  `recvonly` track, `on_frame()` on a `sendonly` one, `pause()`/`resume()` on a `sendonly` track,
  and so on.
</Warning>

***

## Properties

### `name`

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.name -> str
```

The declared name of this track. Never changes.

***

### `kind`

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.kind -> TrackKind | None
```

[`TrackKind.VIDEO`](/sdk-reference/python/types#trackkind-trackdirection) or `TrackKind.AUDIO`, or
`None` before the session has declared its tracks.

***

### `direction`

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.direction -> TrackDirection | None
```

[`TrackDirection.SENDONLY`](/sdk-reference/python/types#trackkind-trackdirection) or
`TrackDirection.RECVONLY`, or `None` before the session has declared its tracks.

***

### `mid`

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.mid -> str | None
```

The SDP media id, once the track has been received. `None` until then, and renegotiated across a
reconnect.

***

### `paused`

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.paused -> bool
```

Whether this track is currently paused. Read live from the session — a `recvonly` track is resumed
automatically on reconnect, so this always reflects the current state rather than a cached one.

***

### `published`

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.published -> bool
```

Whether this `sendonly` slot is currently activated. Always `False` for a `recvonly` track — there
is nothing to activate on a slot that only receives.

Unlike [`paused`](#paused), this is not read live from the session: the session itself doesn't
record it (`publish_track` is a control request and `unpublish_track` a notification, neither leaves
anything to query), so the `Track` tracks it locally. It goes back to `False` on its own when the
connection leaves `ready` — see the note on [`publish()`](#publish) below.

***

## Sending (`sendonly` tracks)

### `publish()`

Activates this `sendonly` slot, so frames pushed into it go on the wire.

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await track.publish() -> Track
```

Until this returns, [`push_frame()`](#push_frame) raises — an unpublished slot has no sender behind
it, so the frames would be accepted and dropped rather than sent.

Returns the track, so getting one and activating it can be a single line:

```python Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
camera = await reactor.track("camera").publish()
camera.push_frame(frame)
```

<Note>
  The activation lasts as long as the session does, not the connection: a reconnect resumes
  `recvonly` tracks automatically but does not restore a `sendonly` track's publish, so publish
  again for anything you were sending after [`reactor.reconnect()`](/sdk-reference/python/reactor#reconnect).
  [`published`](#published) says which side of that you're on.
</Note>

***

### `unpublish()`

Deactivates this `sendonly` slot. Synchronous, unlike the other track methods — it never touches
the network, only a local status check and a fire-and-forget notification.

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.unpublish() -> None
```

<Note>
  Unlike every other operation on `Track`, a failure here is **logged, not raised** — see the
  underlying [`Reactor.unpublish_track()`](/sdk-reference/python/reactor#unpublish_track). Unpublish
  is commonly the last call in a `finally` block; raising there would replace whatever exception was
  already propagating instead of adding to it. Check the logs (`reactor_sdk` at `WARNING`) if a
  track seems to have stayed published.
</Note>

<Note>
  Calling it on a slot that is already not published does nothing — not even a notification to the
  session — deliberately: this is what that same `finally` block often calls after the failure that
  ended the session already cleared the publish, and raising there would replace the exception on
  its way out with this one.
</Note>

***

### `push_frame()`

Pushes one frame into this `sendonly` track. What `data` may be — and what else is needed — follows
from the track's `kind`, which is why there is one method and not two.

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.push_frame(
    data: NDArray[np.uint8] | NDArray[np.int16] | bytes,
    *,
    # video
    width: int | None = None,
    height: int | None = None,
    user_data: bytes | None = None,
    # audio
    sample_rate: int = 48000,
    num_channels: int = 1,
    samples_per_channel: int | None = None,
) -> None
```

<Note>
  Raises [`InvalidStateError`](/sdk-reference/python/types#reactorerror) if the track isn't
  [published](#published) yet — an unpublished slot has no sender behind it, so a pushed frame
  would be accepted and dropped rather than sent. Call [`publish()`](#publish) first.
</Note>

**Video.** An RGB `numpy` array of shape `(height, width, 3)` — exactly what
[`on_frame()`](#on_frame) delivers, needing nothing else:

```python Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
camera.push_frame(frame)
```

Or raw BGRA bytes, which need the dimensions the array would have carried:

```python Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
camera.push_frame(bgra, width=640, height=480)
```

A `(height, width, 4)` array is taken as BGRA already and sent untouched. Pass `user_data` to tag
the frame; it reaches the model as that frame's metadata, dropped unless the model declared that it
reads tags. See [Frame Metadata](/concepts/frame-metadata) for the full round-trip pattern.

**Audio.** Interleaved 16-bit PCM, as bytes or as a `numpy` int16 array. `samples_per_channel` is
worked out from the length when not given:

```python Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
mic.push_frame(pcm, sample_rate=48000, num_channels=1)
```

<Note>
  An argument the track's kind has no use for is refused rather than silently ignored, when ignoring
  it would throw away something the caller meant. `user_data` on an audio track raises `TypeError` —
  the wire format has no metadata trailer for audio, so a tag passed there would simply vanish.
  `sample_rate` on a video track is merely redundant and is let through.
</Note>

***

## Receiving (`recvonly` tracks)

### `on_frame()`

Registers a handler for this track's frames, converted to a NumPy array. Usable as a decorator.

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.on_frame(func: Callable) -> Callable
```

Only this track's frames reach it — every media handler in this SDK is scoped to one track.

On a **video** track, the handler is given as many of `(frame, frame_id, timestamp_us, user_data)`
as it declares parameters for, `frame` being an RGB array of shape `(height, width, 3)`:

```python Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
@output.on_frame
def render(frame): ...

@output.on_frame
def render(frame, frame_id, timestamp_us, user_data): ...
```

On an **audio** track, as many of `(frame, sample_rate, num_channels)`, `frame` being an int16 array
of shape `(samples, channels)`:

```python Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
@speech.on_frame
def play(frame, sample_rate): ...
```

<Note>
  Requires `numpy`, which is not a hard dependency of this package. Use
  [`on_raw_frame()`](#on_raw_frame) for the same frames without the conversion. Registering before
  the session has declared its tracks is allowed — the direction is checked once it's known, and a
  handler that turns out to be on a `sendonly` track simply never fires (logged once as a warning).
</Note>

***

### `on_raw_frame()`

Registers a handler for this track's frames as raw bytes — no NumPy conversion, no NumPy dependency.
Usable as a decorator.

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.on_raw_frame(func: Callable) -> Callable
```

The same routing as [`on_frame()`](#on_frame), without the conversion — every argument the frame
arrived with is passed straight through:

```python Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
@output.on_raw_frame
def forward(bgra, width, height, frame_id, timestamp_us, user_data): ...

@speech.on_raw_frame
def forward(pcm, num_samples, sample_rate, num_channels): ...
```

Every argument is passed — the handler must take them all.

***

### `off_frame()`

Unregisters a handler registered with [`on_frame()`](#on_frame) or
[`on_raw_frame()`](#on_raw_frame).

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
track.off_frame(func: Callable) -> None
```

***

## Pausing (`recvonly` tracks)

### `pause()`

Stops receiving this track. Frames stop arriving until [`resume()`](#resume).

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await track.pause() -> None
```

***

### `resume()`

Starts receiving this track again after [`pause()`](#pause).

```python Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await track.resume() -> None
```

***

## Errors

Calling a method the track's direction doesn't support raises `ValueError` with a message naming the
track, its actual direction, and what to call instead. Calling `push_frame()` or
`pause()`/`resume()` before the session has declared any tracks raises `RuntimeError` telling you to
wait for `READY` or register handlers first. See
[`Reactor.track()`](/sdk-reference/python/reactor#track) for when tracks become resolvable.
