# Camera & Microphone

If you want to see the device management API in action, you can check out [the sample app](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/client/ts-quickstart).

## Camera management

The SDK does its best to make working with the camera easy. We expose the following camera object on the call:

```ts
const camera = call.camera;
```

### Start-stop camera

```typescript
await call.camera.toggle();

// or
await call.camera.enable();
await call.camera.disable();
```

It's always best to await these calls, however the SDK does its best to resolve potential race conditions: the last call always "wins".

Here is how you can access the status:

```typescript
call.camera.state.status; // enabled, disabled or undefined
call.camera.state.status$.subscribe(console.log); // Reactive value for status, you can subscribe to changes
```

Status is updated once the camera is actually enabled or disabled. Use `call.camera.state.optimisticStatus` (and the corresponding observable) for the "optimistic" status that is updated immediately after toggling the camera.

The initial status of the camera is `undefined`. If you don't change the initial status, the default backend settings will be applied once the participant joins the call.

If you're building a lobby screen, this is how you can apply the backend settings:

```typescript
await call.get();
const defaultCameraStatus = call.state.settings?.video.camera_default_on;
defaultCameraStatus ? call.camera.enable() : call.camera.disable();
```

### List and select devices

```typescript
// List devices
// The error handler is called if the user denies permission to use camera
call.camera
  .listDevices()
  .subscribe({ next: console.log, error: console.error });

// Select device
call.camera.select("device Id");
```

Here is how you can access the selected device:

```typescript
call.camera.state.selectedDevice; // currently selected camera
call.camera.state.selectedDevice$.subscribe(console.log); // Reactive value for selected device, you can subscribe to changes
```

### Camera permissions

In a browser, you can use the following API to check whether the user has granted permission to access the connected cameras:

```typescript
call.camera.state.hasBrowserPermission$.subscribe((value) => {
  if (value) {
    // User has granted permission
  } else {
    // User has denied or not yet granted permission
  }
});
```

If you need to react to permission prompts, e.g. to show certain UI while the prompt is pending, subscribe to the `isPromptingPermission$` observable:

```typescript
call.camera.state.isPromptingPermission$.subscribe((value) => {
  if (value) {
    // Permission prompt is visible
  }
});
```

Finally, the `call.camera.state.browserPermissionState$` observable provides granular visibility into the permission state. It has the following values:

| Value       | Description                                       |
| ----------- | ------------------------------------------------- |
| `prompt`    | User hasn't been prompted for this permission yet |
| `prompting` | Permission prompt is visible                      |
| `granted`   | Permission is granted                             |
| `denied`    | Permission is denied                              |

### Camera direction

On mobile devices it's useful if users can switch between the front and back cameras:

```typescript
await call.camera.selectDirection("front"); // or back

// Flip camera
await call.camera.flip();
```

This is how you can access the camera direction:

```typescript
call.camera.state.direction; // front, back or undefined
call.camera.state.direction$.subscribe(console.log); // Reactive value for direction, you can subscribe to changes
```

The initial direction of the camera is `undefined`. If you don't change the initial status, the default backend settings will be applied once the participant joins the call.

If you're building a lobby screen, this is how you can apply the backend settings:

```typescript
await call.get();
// fallback in case no backend setting
let defaultDirection: CameraDirection = "front";
const backendSetting = call.state.settings?.video.camera_facing;
if (backendSetting) {
  defaultDirection = backendSetting === "front" ? "front" : "back";
}
call.camera.selectDirection(defaultDirection);
```

### Render video

#### In call

Follow our [Rendering Video and Audio guide](/video/docs/javascript/guides/playing-video-and-audio/).

#### Lobby preview

This is how you can show a preview video before joining the call:

```typescript
const call = client.call("default", "123");

await call.camera.enable();
const videoEl = document.createElement("video");
videoEl.playsInline = true;
videoEl.muted = true;
videoEl.autoplay = true;

if (videoEl.srcObject !== call.camera.state.mediaStream) {
  videoEl.srcObject = call.camera.state.mediaStream || null;
  if (videoEl.srcObject) {
    videoEl.play();
  }
}
```

### Access to the Camera's MediaStream

Our SDK exposes the current `mediaStream` instance that you can use for your needs (for example, local recording, etc...):

```typescript
// current mediaStream instance
call.camera.state.mediaStream;
// Reactive value for mediaStream, you can subscribe to changes
call.camera.state.mediaStream$.subscribe((mediaStream) => {
  const [videoTrack] = mediaStream.getVideoTracks();
  console.log("Video track", videoTrack);
});
```

## Microphone management

The SDK does its best to make working with the microphone easy. We expose the following microphone object on the call:

```ts
const microphone = call.microphone;
```

### Start-stop microphone

```typescript
await call.microphone.toggle();

// or
await call.microphone.enable();
await call.microphone.disable();
```

It's always best to await these calls, however the SDK does its best to resolve potential race conditions: the last call always "wins".

This is how you can access the microphone status:

```typescript
call.microphone.state.status; // enabled, disabled or undefined
call.microphone.state.status$.subscribe(console.log); // Reactive value for status, you can subscribe to changes
```

Status is updated once the microphone is actually enabled or disabled. Use `call.microphone.state.optimisticStatus` (and the corresponding observable) for the "optimistic" status that is updated immediately after toggling the microphone.

The initial status of the microphone is `undefined`. If you don't change the initial status, the default backend settings will be applied once the participant joins the call.

If you're building a lobby screen, this is how you can apply the backend settings:

```typescript
await call.get();
const defaultMicStatus = call.state.settings?.audio.mic_default_on;
if (defaultMicStatus) {
  call.microphone.enable();
} else {
  call.microphone.disable();
}
```

### List and select devices

```typescript
// List devices
// The error handler is called if the user denies permission to use microphone
call.microphone
  .listDevices()
  .subscribe({ next: console.log, error: console.error });

// Select device
call.microphone.select("device Id");
```

This is how you can access the selected device:

```typescript
call.microphone.state.selectedDevice; // currently selected microphone
call.microphone.state.selectedDevice$.subscribe(console.log); // Reactive value for selected device, you can subscribe to changes
```

### Test a microphone

You can run a quick, on-demand capture test for a specific microphone.
This API is only available in browser environments.

```typescript
const deviceId = call.microphone.state.selectedDevice ?? "default";
const capturesAudio = await call.microphone.performTest(deviceId);

if (!capturesAudio) {
  console.log("Microphone did not capture audio. Check device selection.");
}
```

### Microphone permissions

In a browser, you can use the following API to check whether the user has granted permission to access the connected microphones:

```typescript
call.microphone.state.hasBrowserPermission$.subscribe((value) => {
  if (value) {
    // User has granted permission
  } else {
    // User has denied or not yet granted permission
  }
});
```

If you need to react to permission prompts, e.g. to show certain UI while the prompt is pending, subscribe to the `isPromptingPermission$` observable:

```typescript
call.microphone.state.isPromptingPermission$.subscribe((value) => {
  if (value) {
    // Permission prompt is visible
  }
});
```

Finally, the `call.microphone.state.browserPermissionState$` observable provides granular visibility into the permission state. It has the following values:

| Value       | Description                                       |
| ----------- | ------------------------------------------------- |
| `prompt`    | User hasn't been prompted for this permission yet |
| `prompting` | Permission prompt is visible                      |
| `granted`   | Permission is granted                             |
| `denied`    | Permission is denied                              |

### Play audio

#### In call

Follow our [Playing Video and Audio guide](/video/docs/javascript/guides/playing-video-and-audio/).

#### Lobby preview

On lobby screens a common UX pattern is to show a visual indicator of the detected audio levels coming from the selected microphone. The client exposes the `createSoundDetector` utility method to help implement this functionality. Here is an example of how you can do that:

```html
<progress id="volume" max="100" min="0"></progress>
```

```typescript
import { createSoundDetector } from "@stream-io/video-client";

let cleanup: Function | undefined;
call.microphone.state.mediaStream$.subscribe(async (mediaStream) => {
  const progressBar = document.getElementById("volume") as HTMLProgressElement;
  if (mediaStream) {
    cleanup = createSoundDetector(
      mediaStream,
      (event) => {
        progressBar.value = event.audioLevel;
      },
      { detectionFrequencyInMs: 100 },
    );
  } else {
    await cleanup?.();
    progressBar.value = 0;
  }
});
```

### Speaking while muted notification

When the microphone is disabled, the client will automatically start monitoring audio levels, to detect if the user is speaking.
This feature is enabled by default unless the user doesn't have the permission to send audio or explicitly disabled.

This is how you can subscribe to these notifications:

```typescript
call.microphone.state.speakingWhileMuted; // current value
call.microphone.state.speakingWhileMuted$.subscribe((isSpeaking) => {
  if (isSpeaking) {
    console.log(`You're muted, unmute yourself to speak`);
  }
}); // Reactive value

// to disable this feature completely:
await call.microphone.disableSpeakingWhileMutedNotification();

// to enable it back:
await call.microphone.enableSpeakingWhileMutedNotification();
```

### Access to the Microphone's MediaStream

Our SDK exposes the current `mediaStream` instance that you can use for your needs (for example, local recording, etc...):

```typescript
// current mediaStream instance
call.microphone.state.mediaStream;
// Reactive value for mediaStream, you can subscribe to changes
call.microphone.state.mediaStream$.subscribe((mediaStream) => {
  const [audioTrack] = mediaStream.getAudioTracks();
  console.log("Audio track", audioTrack);
});
```

### Hi-Fi and Stereo Audio

Depending on call type settings set on our dashboard, the `microphone` object can be configured to capture in High Fidelity audio in stereo mode on devices that support it.

One can choose from the following audio modes:

- `VOICE_STANDARD`: standard quality mono audio, suitable for voice calls, default mode
- `VOICE_HIGH_QUALITY`: high-quality mono audio, suitable for podcasts
- `MUSIC_HIGH_QUALITY`: high-quality stereo audio, suitable for music calls

To update the audio mode, use the `setAudioBitrateProfile` method exposed on the `microphone` object:

```tsx
import { SfuModels } from "@stream-io/video-client";

await call.microphone.setAudioBitrateProfile(
  SfuModels.AudioBitrateProfile.MUSIC_HIGH_QUALITY,
);
```

<admonition type="warning">

Please note that once `MUSIC_HIGH_QUALITY` mode is enabled, all automated audio processing
such as echo cancellation and noise suppression are disabled so all surrounding sounds can be captured.

With audio processing disabled, users may experience echo or feedback unless they wear headphones or use a dedicated audio interface.

</admonition>

### Noise Cancellation

Check our [Noise Cancellation](/video/docs/javascript/guides/noise-cancellation/) guide.

## Camera and Microphone AI/ML Filters

Both the Camera and the Microphone allow you to apply AI/ML filters to the media stream.
This can be useful for various use-cases, such as:

- applying a video effects such as background blurring, or background replacement
- applying a custom video filter (e.g. color correction, or face detection)
- applying a custom audio filter (e.g. noise reduction)

```typescript
import { Call } from "@stream-io/video-client";

let call: Call;

// apply a custom video filter
const { unregister: unregisterMyVideoFilter } = camera.registerFilter(
  function myVideoFilter(inputMediaStream: MediaStream) {
    // initialize the video filter, do some magic and
    // return the modified media stream
    return {
      output: mediaStreamWithFilterApplied,
      stop: () => {
        // optional cleanup function
      },
    };
  },
);

// apply a custom audio filter
const { unregister: unregisterMyAudioFilter } = microphone.registerFilter(
  function myAudioFilter(inputMediaStream: MediaStream) {
    // initialize the audio filter, do some magic and
    // return the modified media stream
    return {
      output: mediaStreamWithFilterApplied,
      stop: () => {
        // optional cleanup function
      },
    };
  },
);

// unregister the filters
unregisterMyVideoFilter();
unregisterMyAudioFilter();
```

Filters can be registered and unregistered at any time, and the SDK will take care of the rest.
Filters can be chained, and the order of registration matters.
The first registered filter will be the first to modify the raw `MediaStream`.

A filter may be asynchronous. In this case, the function passed to the `registerFilter` method should _synchronously_
return an object where `output` is a promise that resolves to a `MediaStream`:

```typescript
camera.registerFilter(function myVideoFilter(inputMediaStream: MediaStream) {
  // note that output is returned synchronously!
  return {
    output: new Promise((resolve) => resolve(mediaStreamWithFilterApplied)),
    stop: () => {
      // optional cleanup function
    },
  };
});
```

Registering a filter is not instantaneous. If you need to know when a filter is registered (e.g. to show
a spinner in the UI), await for the `registered` promise returned by the `registerFilter` method:

```typescript
const { registered } = camera.registerFilter(
  (inputMediaStream: MediaStream) => {
    // your video filter
  },
);
await registered;
// at this point, the filter is registered
```

Once a filter(s) is registered, the SDK will send the last returned `MediaStream` to the remote participants.

## Speaker management

### List and select devices

#### Browser support

Selecting audio output device [isn't supported by all browsers](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/sinkId), this is how you can check the availability:

```typescript
call.speaker.state.isDeviceSelectionSupported;
```

```typescript
// List devices
// The error handler is called if the user denies permission to use audio
call.speaker.listDevices().subscribe({
  next: (devices) => console.log(devices),
  error: (err) => console.error(err),
});

// Select device
call.speaker.select("device Id");
```

Device id can also be an empty string, that means to use the system's default audio output.

Here is how you can access the selected device:

```typescript
call.speaker.state.selectedDevice; // currently selected audio output
// Reactive value for selected device, you can subscribe to changes
call.speaker.state.selectedDevice$.subscribe((selectedDevice) => {
  console.log(selectedDevice);
});
```

### Set volume

Volume has to be a number between 0 and 1. 0 means mute the audio output.

```typescript
call.speaker.setVolume(0.5);
```

The default system value is 1.

Here is how you can access the selected device:

```typescript
call.speaker.state.volume; // current volume
// Reactive value for volume, you can subscribe to changes
call.speaker.state.volume$.subscribe((volume) => {
  console.log(volume);
});
```

### Set participant volume

You can control the volume of a specific participant in the call:

```typescript
import { StreamVideoParticipant } from "@stream-io/video-client";

let p: StreamVideoParticipant;

// will set the volume of the participant to 50%
call.speaker.setParticipantVolume(p.sessionId, 0.5);

// will mute the participant
call.speaker.setParticipantVolume(p.sessionId, 0);

// will reset the volume to the default value
call.speaker.setParticipantVolume(p.sessionId, undefined);
```

## Persisting device preferences

By default, device persistence is enabled when `localStorage` is available.
To opt out, or configure it further, adjust the appropriate `options.devicePersistence` flags.

```ts
import { StreamVideoClient } from "@stream-io/video-client";

const client = StreamVideoClient.getOrCreateInstance({
  apiKey,
  user,
  token,
  options: {
    devicePersistence: {
      enabled: true, // false to disable it
      storageKey: "@stream-io/device-preferences", // to override the default
    },
  },
});
```


---

This page was last updated at 2026-07-10T16:05:28.413Z.

For the most recent version of this documentation, visit [https://getstream.io/video/docs/javascript/guides/camera-and-microphone/](https://getstream.io/video/docs/javascript/guides/camera-and-microphone/).