PartialCallStateBuilder<bool>(
call: call,
selector: (state) => state.isWebAudioPlaybackBlocked,
builder: (context, isPlaybackBlocked) {
if (!isPlaybackBlocked) return const SizedBox.shrink();
return const EnableSoundBanner();
},
);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:
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:
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'),
);
}
}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.
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:
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:
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.
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.