> Fetch clean Markdown by appending `.md` to any page URL under https://signalwire.com/docs or requesting it with the HTTP header `Accept: text/markdown`. The root index at https://signalwire.com/docs/llms.txt lists the available documentation indexes. # 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