# Audio Playback on Web

Browsers do not let a page play audio until the user has interacted with it. When that happens on a call, remote participants are audible to everyone except the blocked user, who hears silence with no indication of why. Only a user gesture (a click, tap, or key press) can start playback again, so this is one of the few cases where the SDK cannot recover on its own and needs your UI to help.

This page covers the parts of web audio playback that need code in your app. Everything described here is web only: the state flag is always `false` and the resume method does nothing on iOS, Android, macOS, Windows, and Linux, so you can leave the code in place without platform checks.

## Detecting blocked playback

`CallState.isWebAudioPlaybackBlocked` turns `true` while the browser's autoplay policy is blocking remote audio, and back to `false` once playback starts. Observe it like any other piece of call state:

```dart
PartialCallStateBuilder<bool>(
  call: call,
  selector: (state) => state.isWebAudioPlaybackBlocked,
  builder: (context, isPlaybackBlocked) {
    if (!isPlaybackBlocked) return const SizedBox.shrink();

    return const EnableSoundBanner();
  },
);
```

If you are not using `stream_video_flutter`, the same value is available from `call.state.value.isWebAudioPlaybackBlocked` for a snapshot and `call.partialState((state) => state.isWebAudioPlaybackBlocked)` for a stream.

## Resuming playback

Call `resumeWebAudioPlayback()` from the gesture handler itself. It retries every blocked audio element at once, so a single button is enough no matter how many participants are in the call:

```dart
import 'package:stream_video/stream_video.dart';

class EnableSoundBanner extends StatelessWidget {
  const EnableSoundBanner({super.key});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton.icon(
      onPressed: () => RtcMediaDeviceNotifier.instance.resumeWebAudioPlayback(),
      icon: const Icon(Icons.volume_up),
      label: const Text('Tap to enable sound'),
    );
  }
}
```

<Admonition type="important">

The browser only grants playback while it is handling a user gesture. Calling `resumeWebAudioPlayback()` from `initState`, a timer, or a state listener that reacts to `isWebAudioPlaybackBlocked` will be rejected again. It has to run inside an `onPressed`, `onTap`, or similar handler.

</Admonition>

Where you put the affordance is up to your design. A call-wide banner is the simplest option. If your layout can show it per participant tile, note that the flag is call-wide: it tells you that at least one remote audio element is blocked, not which participant it belongs to.

## Playback problems the SDK recovers on its own

Blocked autoplay is the only case that needs a gesture. When a remote audio element stops for another reason, for example a Bluetooth headset switching profile as the microphone is unmuted, the SDK restarts playback and retries with a backoff on its own. No application code is needed for those, and `isWebAudioPlaybackBlocked` stays `false` throughout.

## Choosing an audio output device

On the web, not every browser lets a page choose which speaker or headset to play to. Check before you show a device picker:

```dart
if (call.checkIfAudioOutputChangeSupported()) {
  // Show the audio output device selector.
}
```

`setAudioOutputDevice` reports whether the browser accepted the device, so handle the failure rather than assuming the switch happened:

```dart
final result = await call.setAudioOutputDevice(device);

result.fold(
  success: (_) {
    // `call.state.value.audioOutputDevice` now holds the new device.
  },
  failure: (failure) {
    showError('Could not switch audio output: ${failure.error.message}');
  },
);
```

A failure means no track could be moved to that device and the previous output is still in use, so keep your picker on the previously selected device. `call.state.value.audioOutputDevice` is only updated when the switch succeeds, and the selection survives remote participants muting and unmuting.

<Admonition type="note">

If you work with tracks directly, `RtcRemoteTrack.setSinkId` returns a `Future<RtcRemoteTrack>` and throws when the browser rejects the device. Most applications should use `call.setAudioOutputDevice` instead, which applies the device to every remote audio track and reports the outcome as a `Result`.

</Admonition>


---

This page was last updated at 2026-08-28T16:17:36.778Z.

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