StreamVideo(
'YOUR_API_KEY',
user: User.regular(userId: userId, name: userName),
userToken: userToken,
options: StreamVideoOptions(
allowMultipleActiveCalls: true,
// Defaults to MultiCallAudioPolicy.suspendExisting. Pick the policy
// that matches your UX. See "Choosing an audio policy" below.
multiCallAudioPolicy: MultiCallAudioPolicy.suspendExisting,
),
);Multicall
In this guide, you'll learn how to keep more than one call joined at the same time and pick the right audio policy for your UX. Two StreamVideoOptions fields, allowMultipleActiveCalls and multiCallAudioPolicy, cover two common patterns:
- Foreground / background rooms. The user joins several rooms and switches which one has mic and speaker focus, without leaving the others. Demonstrated by the video_multicall sample app.
- Pause-on-incoming-call. An in-progress call (for example, a livestream) is paused while a new call (for example, a ringing 1:1) takes over, and resumes automatically when the new call ends. Demonstrated by the video_livestream_with_call sample app.
Before you start, complete the base Flutter Video setup first:
- Installation for SDK dependencies and platform permissions.
- Quickstart for client initialization, authentication, and joining a single call.
This cookbook focuses only on the multicall-specific pieces that differ from basic SDK usage.
Enable multiple active calls
Two StreamVideoOptions fields control the multicall behavior:
allowMultipleActiveCalls: truelets the SDK keep more than one call joined at the same time. Without it, joining a new call leaves the previous one.multiCallAudioPolicydecides how the SDK should behave when a subsequent call is joined while another is already active. This is the part you tune per use case.
Choosing an audio policy
Each Call owns its own native peer-connection factory on iOS, Android, and macOS, and each factory has its own audio device module (ADM). When two calls coexist, both ADMs would otherwise compete for the device's mic and speaker. The policy controls when the SDK calls suspendAudio / resumeAudio on a call automatically:
| Policy | On setActiveCall(newCall) | On removeActiveCall(call) | Best fit |
|---|---|---|---|
suspendExisting (default) | Every other active call gets suspendAudio() | Most-recently-added remaining call gets resumeAudio() | Pause-on-incoming-call. Newest call wins, previous call resumes automatically when it leaves. |
suspendIncoming | The new call gets suspendAudio() (if any call is already active) | No automatic resume | Foreground / background rooms. First-joined call keeps focus, later joins stay muted. |
manual | Nothing | Nothing | You own Call.suspendAudio() / Call.resumeAudio() entirely. |
:::caution
With manual, two unsuspended calls will contend for mic and speaker resources, which usually shows up as poor audio quality or missing audio. Only use it when you have a clear reason to manage suspension yourself.
:::
Call.suspendAudio() / Call.resumeAudio() are still available regardless of policy. The policy only controls what the SDK does automatically on join and leave. You can still call them yourself to drive transitions the policy doesn't model. The most common case is switching between two already-joined calls (see Use case 1 below).
Use case 1: Foreground / background rooms
Full sample: video_multicall
This pattern keeps two (or more) rooms joined at once. The user can switch which room is in focus without disconnecting from the others. Useful for breakout-style conferencing, support handoffs, or any UX where the user needs to monitor several calls but only publish media to one at a time.
The right policy is suspendIncoming:
options: StreamVideoOptions(
allowMultipleActiveCalls: true,
multiCallAudioPolicy: MultiCallAudioPolicy.suspendIncoming,
),Joining a second room in the background
With suspendIncoming, joining a second room is just call.join(). The SDK suspends the new call automatically because another one is already active. Its remote audio arrives muted, and the first-joined room keeps mic and speaker focus.
// Join the first room normally. It becomes the foreground call.
final firstCall = StreamVideo.instance.makeCall(callType: ..., id: ...);
await firstCall.getOrCreate();
await firstCall.join();
// Join the second room. The SDK auto-suspends it because the first room
// is already active. No pre-suspend, post-suspend, or per-track listener
// is needed on the integrator side.
final secondCall = StreamVideo.instance.makeCall(callType: ..., id: ...);
await secondCall.getOrCreate();
await secondCall.join(
connectOptions: CallConnectOptions(
// You typically don't want to publish local media into a background
// room. Disable mic/camera in the connect options.
microphone: TrackOption.disabled(),
camera: TrackOption.disabled(),
),
);Switching focus between rooms
The policy doesn't cover the "swap which existing call is in focus" operation, because no call is joining or leaving. Both stay in activeCalls. You drive that handoff explicitly with suspendAudio / resumeAudio:
Future<void> switchFocus({
required Call from,
required Call to,
}) async {
// Turn off the user's mic/camera on the room losing focus.
await from.setMicrophoneEnabled(enabled: false);
await from.setCameraEnabled(enabled: false);
// Hand the audio session over.
await from.suspendAudio();
await to.resumeAudio();
// Restore the user's mic/camera preferences on the now-active room.
await to.setMicrophoneEnabled(enabled: true);
await to.setCameraEnabled(enabled: true);
}suspendAudio on the outgoing call releases its mic and speaker holds and disables its audio tracks. resumeAudio on the incoming call re-claims them and re-enables the tracks the SDK had disabled when the call was originally backgrounded.
Leaving a room
You don't need any explicit resumeAudio when a room leaves. Under suspendIncoming the leaving call was either:
- the foreground call (no other call needs resuming), or
- a background call (the foreground call was never suspended, so there's nothing to wake up).
If you want the remaining background room to take focus after the foreground room leaves, call resumeAudio on it from your UI code. The policy itself stays out of the way.
Use case 2: Pause-on-incoming-call
Full sample: video_livestream_with_call
This pattern keeps a long-running call going (a livestream, a meeting, an audio room) but pauses its audio whenever a new call takes over. For example, when a viewer rings the livestream host and the host accepts the 1:1 call. When the secondary call ends, the long-running call resumes.
The right policy is the default suspendExisting:
options: StreamVideoOptions(
allowMultipleActiveCalls: true,
// suspendExisting is the default, so you can omit this line. Shown
// for clarity, since this is what makes the pattern work.
multiCallAudioPolicy: MultiCallAudioPolicy.suspendExisting,
),Accepting an incoming call while another is active
The SDK does all the audio work for you. setActiveCall runs at the top of call.join(), which under suspendExisting suspends every other active call. You only need to handle UX concerns (e.g., muting your published mic/camera on the long-running call so its viewers don't see you while you take the side call):
// liveStreamCall is already joined and broadcasting.
StreamVideo.instance.state.incomingCall.listen((incoming) async {
if (incoming == null) return;
// Optional: stop publishing local media on the long-running call while
// the secondary call is active. The SDK handles the audio session
// handoff. This is purely about what your viewers see.
await liveStreamCall.setMicrophoneEnabled(enabled: false);
await liveStreamCall.setCameraEnabled(enabled: false);
// Accept + join the incoming call. setActiveCall runs inside join()
// and the SDK auto-suspends the livestream call.
await incoming.accept();
await incoming.join();
});Hanging up the secondary call
When the secondary call leaves, removeActiveCall runs, and under suspendExisting the SDK auto-resumes the most-recently-added remaining call (the livestream in this example):
// Triggered when the 1:1 call ends.
Future<void> onSecondaryCallEnded() async {
// The SDK has already resumed the livestream's audio session. Re-enable
// the host's publishing tracks so the broadcast continues normally.
await liveStreamCall.setMicrophoneEnabled(enabled: true);
await liveStreamCall.setCameraEnabled(enabled: true);
}You don't need to call resumeAudio yourself. The policy handles it.
Per-call audio configuration
StreamVideoOptions.audioConfigurationPolicy sets the client-wide default that every call uses unless overridden. In a multi-call setup you sometimes need different rooms to use different audio profiles. For example, a livestream for a viewer with ViewerAudioPolicy and a 1:1 call between host and participant with BroadcasterAudioPolicy on the same client.
Pass audioConfigurationPolicy on DefaultCallPreferences when creating the Call, and the per-call factory will be built with that policy instead of the client default:
final livestreamRoom = streamVideo.makeCall(
callType: StreamCallType.livestream(),
id: 'livestream',
preferences: DefaultCallPreferences(
audioConfigurationPolicy: const AudioConfigurationPolicy.viewer(),
),
);
final callRoom = streamVideo.makeCall(
callType: StreamCallType.defaultType(),
id: '1-1call',
preferences: DefaultCallPreferences(
audioConfigurationPolicy: const AudioConfigurationPolicy.broadcaster(),
),
);Each call's native peer-connection factory is built with its own policy at first use (when you call Call.join() or render the lobby preview). The policies don't conflict across the two factories. The only shared resource is the platform audio session, which the SDK auto-coordinates through the suspend / resume flow described above.
If audioConfigurationPolicy is omitted from DefaultCallPreferences, the call falls back to StreamVideoOptions.audioConfigurationPolicy (or BroadcasterAudioPolicy if that's also unset).
Summary
StreamVideoOptions(allowMultipleActiveCalls: true)enables joining more than one call.multiCallAudioPolicypicks the audio-handoff strategy. DefaultsuspendExistingmatches "newest call wins" UX (pause-on-incoming-call).suspendIncomingkeeps the first-joined call in focus and auto-mutes later joins (foreground / background rooms).manualopts out, leavingsuspendAudio/resumeAudioentirely to you.- You only ever call
Call.suspendAudio()/Call.resumeAudio()yourself to drive transitions the policy doesn't model. Most commonly when switching focus between two already-joined calls. DefaultCallPreferences(audioConfigurationPolicy: ...)overridesStreamVideoOptions.audioConfigurationPolicyper call, so different rooms (e.g. a viewer livestream and a broadcaster 1:1) can use different audio profiles on the same client.- Two reference implementations live in the flutter-video-samples repo:
- video_multicall:
suspendIncoming, split-room UI. - video_livestream_with_call:
suspendExisting(default), livestream + ringing 1:1 call.
- video_multicall: