Skip to main content
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.
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:
options.forceDiscardTrack
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.
// Stop the camera track entirely when muting
call.setLocalAudio(false, { forceDiscardTrack: true });
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.

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.
// 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:
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.
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:
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 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):
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:
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:
audioDeviceId
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.
videoDeviceId
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.
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() with audio.settings or video.settings instead:
// 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:
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 });
}
setOutputDeviceAsync() is only supported in browsers that implement 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.

Cycling camera and mic

For mobile or simple use cases, cycleCamera() and cycleMic() rotate through available devices without you having to enumerate them manually:
// 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:
const call = Daily.createCallObject({
  dailyConfig: { noAutoDefaultDeviceChange: true },
});
When disabled, you can listen for 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:
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.
1

Start the observer

// Poll every 100 ms (default)
await call.startLocalAudioLevelObserver();

// Or specify a custom interval in milliseconds
await call.startLocalAudioLevelObserver(200);
2

Listen for audio level events

call.on('local-audio-level', ({ audioLevel }) => {
  // audioLevel is a number from 0.0 to 1.0
  micMeter.style.width = `${audioLevel * 100}%`;
});
3

Read the level imperatively (optional)

// Any time after the observer is started:
const level = call.getLocalAudioLevel(); // 0.0–1.0
4

Stop the observer when no longer needed

call.stopLocalAudioLevelObserver();
// 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):
// 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();
const isRemoteRunning = call.isRemoteParticipantsAudioLevelObserverRunning();

Complete device-switcher example

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));
    }
  });
}