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

# Audio and Video Controls

> Manage local and remote audio/video tracks, switch input and output devices, and monitor audio levels in a Daily call.

Daily gives you fine-grained control over microphone and camera state, device selection, and audio level monitoring — both before and during a call.

## Muting and unmuting

Check the current mute state with `localAudio()` and `localVideo()`, then toggle with the matching setters.

```javascript theme={null}
const call = Daily.createCallObject();

// Read current state
const isMicOn = call.localAudio();   // true = mic is sending
const isCamOn = call.localVideo();   // true = camera is sending

// Mute the microphone
call.setLocalAudio(false);

// Unmute the microphone
call.setLocalAudio(true);

// Turn the camera off
call.setLocalVideo(false);
```

`setLocalAudio` accepts an optional second argument:

<ParamField path="options.forceDiscardTrack" type="boolean">
  When `true`, the underlying `MediaStreamTrack` is discarded (stopped) on mute rather than kept alive. Use this when you want the microphone indicator to turn off on mute. If not set, defaults to the `keepCamIndicatorLightOn` value from `dailyConfig`, or `false` if that is not set.
</ParamField>

```javascript theme={null}
// Stop the camera track entirely when muting
call.setLocalAudio(false, { forceDiscardTrack: true });
```

<Warning>
  Setting `forceDiscardTrack: true` for audio can cause the beginning of a participant's audio to be clipped when they unmute. Because the track is fully discarded, remote participants must complete the entire track subscription process again when the sender unmutes — rather than simply resuming a paused track.
</Warning>

## Starting camera before joining

Call `startCamera()` to acquire device permissions and start the camera/mic pipeline before the user joins a room. This lets you display a preview and confirm device selection on a pre-join screen.

```javascript theme={null}
// Request permissions and start preview
const devices = await call.startCamera();
console.log('Camera:', devices.camera);
console.log('Mic:', devices.mic);

// Later, join with camera already running
await call.join({ url: 'https://your-domain.daily.co/room-name' });
```

You can also pass `DailyCallOptions` to `startCamera()` to pre-configure devices or input settings:

```javascript theme={null}
await call.startCamera({
  inputSettings: {
    video: { settings: { facingMode: { exact: 'environment' } } },
    audio: { processor: { type: 'noise-cancellation' }},
  },
});
```

## Joining with audio or video off

Set `startAudioOff` and/or `startVideoOff` to `false` in your call configuration before joining or starting the camera to have the participant join with their mic and/or camera off and override any room or token settings.

```javascript theme={null}
await call.join({
  url: 'https://your-domain.daily.co/room-name',
  startAudioOff: false,
  startVideoOff: false,
});
```

## Enumerating available devices

Use `enumerateDevices()` to get all available media devices:

```javascript theme={null}
const { devices } = await call.enumerateDevices();

const cameras = devices.filter(d => d.kind === 'videoinput');
const mics    = devices.filter(d => d.kind === 'audioinput');
const speakers = devices.filter(d => d.kind === 'audiooutput');

cameras.forEach(cam => {
  console.log(cam.label, cam.deviceId, cam.facing); // facing: 'user'|'environment'
});
```

Each entry is a `DailyMediaDeviceInfo` — a superset of [`MediaDeviceInfo`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo) with an optional `facing` field (`'user'` | `'environment'`) for cameras on mobile devices.

## Reading the active devices

`getInputDevices()` returns the devices currently in use (or expected to be used):

```javascript theme={null}
const { camera, mic, speaker } = await call.getInputDevices();
// Each is {} if no device is active, or a MediaDeviceInfo object
console.log('Active camera:', camera);
```

The return type is `DailyDeviceInfos`:

```typescript theme={null}
interface DailyDeviceInfos {
  camera:  {} | DailyMediaDeviceInfo;
  mic:     {} | MediaDeviceInfo;
  speaker: {} | MediaDeviceInfo;
}
```

## Switching input devices

### By device ID

Use `setInputDevicesAsync()` to switch the active camera or microphone by `deviceId`:

<ParamField path="audioDeviceId" type="string | false">
  The `deviceId` of the microphone to use. Setting to `false` will turn off the microphone and disallow the user to turn it back on until a valid `deviceId` or `true` is passed.
</ParamField>

<ParamField path="videoDeviceId" type="string | false">
  The `deviceId` of the camera to use. Setting to `false` will turn off the camera and disallow the user to turn it back on until a valid `deviceId` or `true` is passed.
</ParamField>

```javascript theme={null}
document.getElementById('camera-select').addEventListener('change', async (e) => {
  await call.setInputDevicesAsync({ videoDeviceId: e.target.value });
});

document.getElementById('mic-select').addEventListener('change', async (e) => {
  await call.setInputDevicesAsync({ audioDeviceId: e.target.value });
});
```

### With constraints or a custom track

For anything beyond a simple device ID swap — such as applying media constraints or supplying a custom `MediaStreamTrack` — use [`updateInputSettings()`](/reference/daily-js/instance-methods/update-input-settings) with `audio.settings` or `video.settings` instead:

```javascript theme={null}
// Apply custom constraints to the camera
await call.updateInputSettings({
  video: {
    settings: {
      deviceId: { exact: 'your-camera-device-id' },
      aspectRatio: 1.0
    },
  },
});

// Use a pre-captured MediaStreamTrack as the camera source
const [track] = canvasStream.getVideoTracks();
await call.updateInputSettings({
  video: { settings: { customTrack: track } },
});
```

## Switching the output device

Use `setOutputDeviceAsync()` to change the speaker or audio output device:

```javascript theme={null}
const speakers = devices.filter(d => d.kind === 'audiooutput');
const headphones = speakers.find(d => d.label.includes('Headphones'));

if (headphones) {
  await call.setOutputDeviceAsync({ outputDeviceId: headphones.deviceId });
}
```

<Note>
  `setOutputDeviceAsync()` is only supported in browsers that implement [`HTMLMediaElement.setSinkId()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId). Most non-Android browsers now support this. Safari only recently added support in 18.4 and may have some quirks. Safari also requires a user gesture to switch audio output devices, so be sure to call this from a click handler or similar.
</Note>

## Cycling camera and mic

For mobile or simple use cases, `cycleCamera()` and `cycleMic()` rotate through available devices without you having to enumerate them manually:

```javascript theme={null}
// Flip between front and rear camera
const { device } = await call.cycleCamera();
console.log('Now using:', device?.label);

// Prefer switching to a different facing mode (front ↔ rear)
const { device: nextCam } = await call.cycleCamera({
  preferDifferentFacingMode: true,
});

// Cycle to the next microphone
const { device: nextMic } = await call.cycleMic();
```

## Automatic device switching

Daily automatically handles two common device disruption scenarios without any code on your part:

* **System default changes** — if the active device is set to the system default (Chrome uses `deviceId: "default"`; other browsers use the first device in the list) and the user changes their system default, Daily switches to the new default.
* **Active device disconnected** — if the currently selected device is unplugged or becomes unavailable, Daily switches to the next available device.

To disable this behavior and manage device changes yourself, set `noAutoDefaultDeviceChange: true` in `dailyConfig`:

```javascript theme={null}
const call = Daily.createCallObject({
  dailyConfig: { noAutoDefaultDeviceChange: true },
});
```

When disabled, you can listen for [`available-devices-updated`](/reference/daily-js/events/settings-events#available-devices-updated) to respond to device changes manually.

## Device change events

Listen for `available-devices-updated` when the system's device list changes (e.g., a USB camera is plugged in), and `selected-devices-updated` when the active devices change:

```javascript theme={null}
call.on('available-devices-updated', ({ availableDevices }) => {
  // availableDevices: MediaDeviceInfo[]
  console.log('Device list changed:', availableDevices);
  refreshDeviceSelectors(availableDevices);
});

call.on('selected-devices-updated', ({ devices }) => {
  // devices: DailyDeviceInfos
  console.log('Active camera:', devices.camera);
  console.log('Active mic:', devices.mic);
  console.log('Active speaker:', devices.speaker);
});
```

## Monitoring local audio level

Daily can poll the local participant's microphone volume and emit it as an event, which is useful for building a mic-level indicator.

<Steps>
  <Step title="Start the observer">
    ```javascript theme={null}
    // Poll every 100 ms (default)
    await call.startLocalAudioLevelObserver();

    // Or specify a custom interval in milliseconds
    await call.startLocalAudioLevelObserver(200);
    ```
  </Step>

  <Step title="Listen for audio level events">
    ```javascript theme={null}
    call.on('local-audio-level', ({ audioLevel }) => {
      // audioLevel is a number from 0.0 to 1.0
      micMeter.style.width = `${audioLevel * 100}%`;
    });
    ```
  </Step>

  <Step title="Read the level imperatively (optional)">
    ```javascript theme={null}
    // Any time after the observer is started:
    const level = call.getLocalAudioLevel(); // 0.0–1.0
    ```
  </Step>

  <Step title="Stop the observer when no longer needed">
    ```javascript theme={null}
    call.stopLocalAudioLevelObserver();
    ```
  </Step>
</Steps>

```javascript theme={null}
// Check whether the observer is currently running
const isRunning = call.isLocalAudioLevelObserverRunning();
```

## Monitoring remote participants' audio levels

To monitor the audio levels of all remote participants simultaneously (useful for displaying speaking indicators):

```javascript theme={null}
// Start polling remote audio levels every 150 ms
await call.startRemoteParticipantsAudioLevelObserver(150);

call.on('remote-participants-audio-level', ({ participantsAudioLevel }) => {
  // participantsAudioLevel: { [sessionId: string]: number }
  for (const [sessionId, level] of Object.entries(participantsAudioLevel)) {
    const tile = document.getElementById(`tile-${sessionId}`);
    if (tile) {
      tile.classList.toggle('speaking', level > 0.05);
    }
  }
});

// Read levels imperatively
const levels = call.getRemoteParticipantsAudioLevel();

// Stop when done
call.stopRemoteParticipantsAudioLevelObserver();
```

```javascript theme={null}
const isRemoteRunning = call.isRemoteParticipantsAudioLevelObserverRunning();
```

## Audio and video processing

[`updateInputSettings()`](/reference/daily-js/instance-methods/update-input-settings) applies real-time processing to your audio and video tracks. You can pass `inputSettings` directly to `join()` or `startCamera()` to enable processing before the first frame is captured, or call it at any time during a call.

### Noise cancellation

```javascript theme={null}
await call.updateInputSettings({
  audio: { processor: { type: 'noise-cancellation' } },
});

// Disable
await call.updateInputSettings({
  audio: { processor: { type: 'none' } },
});
```

### Background blur

`strength` ranges from `0` (no blur) to `1` (maximum blur):

```javascript theme={null}
await call.updateInputSettings({
  video: {
    processor: { type: 'background-blur', config: { strength: 0.75 } },
  },
});
```

### Background image replacement

```javascript theme={null}
await call.updateInputSettings({
  video: {
    processor: {
      type: 'background-image',
      config: { source: 'https://example.com/office.jpg' },
    },
  },
});
```

### Face detection

Enables face detection processing. Detected face counts are reported via the `face-counts-updated` event:

```javascript theme={null}
await call.updateInputSettings({
  video: { processor: { type: 'face-detection' } },
});
```

### Enabling at join time

```javascript theme={null}
await call.join({
  url: roomUrl,
  inputSettings: {
    audio: { processor: { type: 'noise-cancellation' } },
    video: { processor: { type: 'background-blur', config: { strength: 0.5 } } },
  },
});
```

Listen for [`input-settings-updated`](/reference/daily-js/events/settings-events#input-settings-updated) to track changes:

```javascript theme={null}
call.on('input-settings-updated', ({ inputSettings }) => {
  console.log('Input settings changed:', inputSettings);
});
```

<Note>
  Video processors require browser support for `OffscreenCanvas` and WebGL. Check `Daily.supportedBrowser().supportsVideoProcessing` before enabling them.
</Note>

## Complete device-switcher example

```javascript theme={null}
async function buildDeviceSwitcher(call) {
  const { devices } = await call.enumerateDevices();

  const camSelect = document.getElementById('camera-select');
  const micSelect = document.getElementById('mic-select');
  const spkSelect = document.getElementById('speaker-select');

  // Populate selects
  for (const d of devices) {
    const opt = new Option(d.label || d.deviceId, d.deviceId);
    if (d.kind === 'videoinput')  camSelect.append(opt.cloneNode(true));
    if (d.kind === 'audioinput')  micSelect.append(opt.cloneNode(true));
    if (d.kind === 'audiooutput') spkSelect.append(opt.cloneNode(true));
  }

  camSelect.onchange = () =>
    call.setInputDevicesAsync({ videoDeviceId: camSelect.value });

  micSelect.onchange = () =>
    call.setInputDevicesAsync({ audioDeviceId: micSelect.value });

  spkSelect.onchange = () =>
    call.setOutputDeviceAsync({ outputDeviceId: spkSelect.value });

  // Keep in sync with hot-plug events
  call.on('available-devices-updated', ({ availableDevices }) => {
    // Re-populate selects with the updated device list
    [camSelect, micSelect, spkSelect].forEach(s => (s.innerHTML = ''));
    for (const d of availableDevices) {
      const opt = new Option(d.label || d.deviceId, d.deviceId);
      if (d.kind === 'videoinput')  camSelect.append(opt.cloneNode(true));
      if (d.kind === 'audioinput')  micSelect.append(opt.cloneNode(true));
      if (d.kind === 'audiooutput') spkSelect.append(opt.cloneNode(true));
    }
  });
}
```
