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

# Tracks and Media

> Understand DailyTrackState, track states, subscriptions, and how to attach tracks to video elements.

Daily models every media stream — camera, microphone, screen share, and custom tracks — through the `DailyTrackState` interface. Understanding track states is essential for building a reliable call object UI.

## DailyTrackState

See the [`DailyTrackState` reference](/reference/daily-js/types/daily-track-state) for the full interface definition and field descriptions.

## Track States

The `state` field describes the current condition of a track — not a step in a sequential progression:

| State         | Meaning                                                                                                                                          |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `blocked`     | The track cannot be sent. Check `blocked.byDeviceMissing`, `byDeviceInUse`, or `byPermissions` for the cause.                                    |
| `off`         | The track is intentionally not being sent. Check the `off` sub-fields to distinguish an implicit user mute from other causes like remote muting. |
| `sendable`    | The track is ready to be sent, but has not been subscribed to, and is neither blocked nor off.                                                   |
| `loading`     | Daily is acquiring the device or establishing the stream.                                                                                        |
| `interrupted` | The track was previously playable but has temporarily stalled — common during network fluctuations or tab backgrounding on iOS.                  |
| `playable`    | The track is live and renderable.                                                                                                                |

### Why the `off` sub-fields matter

```typescript theme={null}
const audioState = participant.tracks.audio;

if (audioState.state === 'off') {
  if (audioState.off?.byUser) {
    // Participant muted themselves
  } else if (audioState.off?.byRemoteRequest) {
    // Owner muted them remotely
  } else if (audioState.off?.byBandwidth) {
    // Daily throttled the track due to bandwidth
  } else if (audioState.off?.byCanSendPermission) {
    // Participant lacks permission to send this track
  } else if (audioState.off?.byServerLimit) {
    // Server-side participant limit reached
  }
}
```

## Track Types

Each `DailyParticipant` carries a `tracks` object of type `DailyParticipantTracks`:

```typescript theme={null}
interface DailyParticipantTracks {
  audio: DailyTrackState;         // microphone
  video: DailyTrackState;         // camera
  screenAudio: DailyTrackState;   // screen share audio
  screenVideo: DailyTrackState;   // screen share video
  [customTrackKey: string]: DailyTrackState | undefined; // custom tracks
}
```

### Custom tracks

Custom tracks let you send arbitrary `MediaStreamTrack` objects alongside the standard camera and mic. Start them with `startCustomTrack()` and stop them with `stopCustomTrack()`:

```typescript theme={null}
const trackName = await call.startCustomTrack({
  track: myCanvasStream.getVideoTracks()[0],
  trackName: 'my-canvas',
});

// Accessible on the local participant at:
// call.participants().local.tracks['my-canvas']

await call.stopCustomTrack(trackName);
```

Custom track keys appear as string index entries in `DailyParticipantTracks`.

## Local vs. remote track behavior

How a track's initial state is reached differs between local and remote:

* **Local tracks** start directly as `playable`, `off`, or `blocked` — they never pass through `loading`. If a local track starts `off` and the user enables it with `setLocalVideo(true)` or `setLocalAudio(true)`, it transitions straight to `playable` (or `blocked` if the user denied device permissions).
* **Remote tracks** can start in any state except `playable` or `interrupted` — most commonly `loading` (subscribed but stream not yet established), `off` (sender muted before you joined), `blocked` (sender has a device issue), or `sendable` (sender is ready but you haven't subscribed).
* **`interrupted`** is never an initial state — it only occurs after a track was `playable` and then transiently stalled, most commonly during network fluctuations or iOS tab backgrounding. It can return to `playable` once the condition resolves.

## Track Subscription

By default Daily subscribes to tracks from all participants automatically (`subscribeToTracksAutomatically: true`). In large calls, subscribing to every track is wasteful. Disable automatic subscription to control this explicitly.

```typescript theme={null}
const call = Daily.createCallObject({
  subscribeToTracksAutomatically: false,
});
```

### `DailyTrackSubscriptionState`

```typescript theme={null}
type DailyTrackSubscriptionState = 'staged' | boolean;
```

| Value      | Meaning                                                                                                                                  |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `true`     | Subscribed to the track (receiving it).                                                                                                  |
| `false`    | Not subscribed.                                                                                                                          |
| `'staged'` | Receive metadata (SDP negotiated) but do not decode. Staged tracks can then transition to `playable` faster when you set them to `true`. |

### `DailyTrackSubscriptionOptions`

```typescript theme={null}
type DailyTrackSubscriptionOptions =
  | DailyTrackSubscriptionState    // applies to all tracks for participant
  | {
      audio?: DailyTrackSubscriptionState;
      video?: DailyTrackSubscriptionState;
      screenVideo?: DailyTrackSubscriptionState;
      screenAudio?: DailyTrackSubscriptionState;
      custom?: DailyCustomTrackSubscriptionState;
    };
```

### Subscribing per participant

```typescript theme={null}
// Subscribe to audio only for a participant
call.updateParticipant(sessionId, {
  setSubscribedTracks: {
    audio: true,
    video: false,
    screenVideo: false,
    screenAudio: false,
  },
});

// Subscribe to everything for a specific participant
call.updateParticipant(sessionId, {
  setSubscribedTracks: true,
});

// Stage video for everyone (efficient on-demand loading)
call.updateParticipants('*',
  { setSubscribedTracks: { video: 'staged', audio: true } }
);
```

### Global subscription toggle

```typescript theme={null}
// Re-enable automatic subscription after disabling it
call.setSubscribeToTracksAutomatically(true);

// Check current value
console.log(call.subscribeToTracksAutomatically());
```

<Warning>
  Calling `setSubscribeToTracksAutomatically(false)` during a call will unsubscribe you from all current tracks and require you to manually re-subscribe. Use with caution to avoid unintended media loss. For most cases, it's best to set this at call creation time and manage subscriptions explicitly from the start.
</Warning>

## Attaching Tracks to Video Elements

The recommended approach for rendering tracks is to use `track-started` and `track-stopped` events:

```typescript theme={null}
const videoElements: Map<string, HTMLVideoElement> = new Map();

call.on('track-started', ({ participant, track, type }) => {
  let videoEl = videoElements.get(participant.session_id);
  if (!videoEl) {
    videoEl = document.createElement('video');
    videoEl.autoplay = true;
    videoEl.playsInline = true;
    document.getElementById('grid')?.appendChild(videoEl);
    videoElements.set(participant.session_id, videoEl);
  }

  videoEl.srcObject = new MediaStream([track]);
});

call.on('track-stopped', ({ participant, track }) => {
  // Clear srcObject when track stops
  const videoEl = videoElements.get(participant?.session_id ?? '');
  if (videoEl) {
    videoEl.srcObject = null;
  }
});
```

### `track` vs. `persistentTrack`

The key difference: `track` is `undefined` whenever `state !== 'playable'`. `persistentTrack` is present whenever a track exists, regardless of playable state.

We recommend using `persistentTrack` over `track` when attaching to media elements. It acts as a proactive defense against black frames during call disruptions and helps avoid browser bugs related to auto-playing media tracks.

```typescript theme={null}
call.on('participant-updated', ({ participant }) => {
  const trackState = participant.tracks.video;
  const track = trackState.persistentTrack;

  if (track) {
    videoEl.srcObject = new MediaStream([track]);
  } else {
    videoEl.srcObject = null;
  }
});
```

Use `track` only when you need a guarantee that the media is currently flowing.

## Track Events

| Event                 | Fires when                                                          | Event object                       |
| --------------------- | ------------------------------------------------------------------- | ---------------------------------- |
| `track-started`       | A track becomes `playable`, or a new track replaces an existing one | `{ participant, track, type }`     |
| `track-stopped`       | A track leaves `playable` state, or is replaced by a new track      | `{ participant, track, type }`     |
| `participant-updated` | Any track state changes                                             | `{ participant }` — check `tracks` |

The `type` field in `DailyEventObjectTrack` is one of `'video'`, `'audio'`, `'screenVideo'`, `'screenAudio'`, or a custom track name string.

### Track replacement

`track-started` and `track-stopped` fire not only on state transitions but also when the underlying `MediaStreamTrack` is replaced — for example when a participant switches cameras or a device change causes a new track to be issued. In this case the track state stays `playable` throughout, but you will receive a `track-stopped` for the old track immediately followed by a `track-started` for the new one.

This means you should key your media elements on the `track` object itself (or its `id`), not on participant or track type alone:

```typescript theme={null}
call.on('track-stopped', ({ track, type, participant }) => {
  // Remove the specific track that stopped, not just any track for this participant
  const el = document.getElementById(`track-${track.id}`);
  el?.remove();
});

call.on('track-started', ({ track, type, participant }) => {
  const el = document.createElement(type === 'audio' ? 'audio' : 'video');
  el.id = `track-${track.id}`;
  el.autoplay = true;
  el.playsInline = true;
  el.srcObject = new MediaStream([track]);
  document.getElementById(`participant-${participant?.session_id}`)?.appendChild(el);
});
```
