> For a complete index of all SignalWire documentation pages, fetch https://signalwire.com/docs/llms.txt

# Client Preferences

`client.preferences` holds **per-client defaults** the SDK reads when
no per-call options override them: which mic / camera to use, whether
to receive video by default, ICE / recovery tuning, codec ordering,
and custom `userVariables` attached to every call. Preferences live
in the browser, optionally persist to `localStorage`, and are
distinct from per-User configuration (which lives on the
platform — see [Users](/docs/browser-sdk/v4/guides/users)).

This page covers how preferences fit into the SDK lifecycle. For the
full property list, see [`ClientPreferences`].

## Defaults vs. per-call overrides

```text
client.preferences  ←  defaults
        ↓
client.dial(dest, options)  ←  per-call overrides win
```

Anything set on `preferences` applies to every subsequent
[`dial()`][`SignalWire.dial()`] that doesn't pass a competing field.
Per-call options always win:

```js
client.preferences.receiveVideo = true;      // receive remote video by default

// This one call stays audio-only:
await client.dial("/private/team", { receiveVideo: false });
```

Use `preferences` for app-wide defaults (codec ordering, a tier-wide
`userVariables` payload). Use per-call options for situational values.

For example, codec ordering is an app-wide default — the array is a priority
list of [codec names](/docs/browser-sdk/v4/reference/client-preferences/preferred-audio-codecs), and it's overridable per call:

```js
// Prefer Opus, fall back to G.711
client.preferences.preferredAudioCodecs = ["opus", "PCMU"];

// One call insists on G.711:
await client.dial("/private/team", { preferredAudioCodecs: ["PCMU"] });
```

## Common preferences

The full surface is documented in the [`ClientPreferences`] reference. These are the ones most
apps touch, with their code defaults:

| Preference                     | Default      | Controls                                  |
| ------------------------------ | ------------ | ----------------------------------------- |
| `receiveVideo`                 | `false`      | whether to accept inbound video on a call |
| `preferredAudioCodecs`         | `[]`         | audio codec priority order                |
| `connectionTimeout`            | `10` (s)     | WebSocket connect timeout                 |
| `degradationBitrateThreshold`  | `150` (kbps) | bitrate below which video auto-disables   |
| `degradationRecoveryThreshold` | `300` (kbps) | bitrate above which video re-enables      |

## Persistence

By default, preferences live in memory only. Set
`savePreferences: true` to hydrate from `localStorage` on startup and
write back on every setter:

```js
const client = new SignalWire(provider, { savePreferences: true });
```

The following details are persisted: timeouts, ICE / recovery tuning, codec preferences, stats and
device-management flags, and `userVariables`.

**Device selections persist separately, and are on by default.** Independent of
`savePreferences`, the device controller writes your mic / camera / speaker
selections to `localStorage` (keyed by `deviceId`, keeping `label` / `groupId` to
re-match when IDs rotate across sessions) and restores them next time. This is
governed by the [`persistDeviceSelection`](/docs/browser-sdk/v4/reference/client-preferences/persist-device-selection)
preference (default `true`); set it to `false` to opt out.

For a different storage backend (IndexedDB, server-side per user),
leave `savePreferences` off and mirror manually:

```ts
function setReceiveVideo(value: boolean) {
  client.preferences.receiveVideo = value;
  myStore.set("receiveVideo", value);
}
```

`ClientPreferences` is a synchronous object — there is no `update$`
observable. Preferences are read at dial time.

## `userVariables`

`userVariables` is a free-form payload attached to every outbound call.
The receiving side (an AI agent, a SWML script, a backend) reads it.

```js
client.preferences.userVariables = {
  plan:   user.plan,
  locale: navigator.language,
};
```

Set on preferences for app-wide values; pass to `dial()` for per-call
attribution.

## Time units

Timeouts on the preferences surface are exposed in **seconds** (stored
as milliseconds internally):

```js
client.preferences.connectionTimeout = 30;   // 30 seconds
client.preferences.iceRestartTimeout = 10;   // 10 seconds
```

Other fields use the unit of the underlying API (kbps, integer
levels, etc.).

## Keyframe recovery

A video stream consists of occasional **keyframes** — complete, self-contained
frames — each followed by **delta frames** that encode only the change from the
previous frame. A lost or corrupted delta frame corrupts every frame after it
until the next keyframe arrives. The receiver can request one early via an RTCP
feedback message:

* [**PLI** — Picture Loss Indication](https://www.rfc-editor.org/rfc/rfc4585#section-6.3.1):
  standard picture-loss recovery.
* [**FIR** — Full Intra Request](https://www.rfc-editor.org/rfc/rfc5104#section-4.3.1):
  forces a full intra frame from scratch, e.g. when a new participant or
  recorder joins mid-stream with no reference frame.

Keyframes are large, so the SDK rate-limits these requests as a burst with
cooldown:

| Preference                                                                                       | Default    | Role                          |
| ------------------------------------------------------------------------------------------------ | ---------- | ----------------------------- |
| [`keyframeMaxBurst`](/docs/browser-sdk/v4/reference/client-preferences/keyframe-max-burst)       | `3`        | max requests per window       |
| [`keyframeBurstWindow`](/docs/browser-sdk/v4/reference/client-preferences/keyframe-burst-window) | `3000` ms  | length of the counting window |
| [`keyframeCooldown`](/docs/browser-sdk/v4/reference/client-preferences/keyframe-cooldown)        | `10000` ms | pause once the burst is spent |

Up to `keyframeMaxBurst` requests are allowed per `keyframeBurstWindow`; once
that limit is hit, requests pause for `keyframeCooldown`. Defaults: three
requests per three-second window, then a ten-second cooldown.

## Reference

* [`ClientPreferences`] — the property surface
* [`SignalWire.preferences`] — the instance
* [`SignalWireOptions`] — `savePreferences`, `skipDeviceMonitoring`, `reconnectAttachedCalls`, `persistSession`
* [`SignalWire.dial()`] — per-call overrides

[`ClientPreferences`]: /docs/browser-sdk/v4/reference/client-preferences

[`SignalWire.preferences`]: /docs/browser-sdk/v4/reference/signalwire

[`SignalWireOptions`]: /docs/browser-sdk/v4/reference/interfaces/signalwire-options

[`SignalWire.dial()`]: /docs/browser-sdk/v4/reference/signalwire/dial