Change Mode - Flutter
In a live stream, audience members usually join in RECV_ONLY mode, meaning they can only view and listen to the hosts. However, if a host invites an audience member to actively participate (e.g., to speak or present), the audience member can switch their mode to SEND_AND_RECV using the changeMode() method.
This guide explains how to use the changeMode() method and walks through a sample implementation where a host invites an audience member to become a host using PubSub.
changeMode() only ever changes the mode of the participant who calls it. There is
no form of changeMode() that takes another participant's id, and a host cannot change
an audience member's mode directly.
Promotion therefore takes two pieces of code, one on each side, and neither does anything on its own:
- On the host — publish a request to the invited participant over PubSub. Step 1 and Step 2 below.
- On the invited participant — subscribe to that same topic and, in the message
handler, call
changeMode()yourself. Step 3 below. Without this subscriber the request reaches nobody and no mode ever changes.
An implementation that only publishes the request looks finished and does nothing.
changeMode()
- The
changeMode()method from theRoomclass switches the calling participant's own mode during a live stream — for example, an audience member becoming a host. - It takes only the target mode. To move a different participant between modes, send them a PubSub request and let their client call
changeMode()itself, as shown in the Implementation Guide below. - It returns a
Future, so await it insidetry/catch. It throws aVideoSDKErrorwhen the change fails, for example3054(ERROR_ALREADY_IN_REQUESTED_STATE) when the participant is already in that mode, or3055(ERROR_OPERATION_IN_PROGRESS) while an earlierchangeMode()call is still running. - On Android, switching to
Mode.SEND_AND_RECVdoes not ask for camera or microphone permission. CallVideoSDK.requestPermissions()first, or the camera and microphone stay off. localParticipant.modechanges only once the server applies the new mode, just before theparticipantModeChangedevent fires. The example below updates its button from that event, not whenchangeMode()returns. The same event reports other participants' modes, as described in Updating the UI on mode change.
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
class LiveStreamScreen extends StatefulWidget {
const LiveStreamScreen({super.key});
@override
State<LiveStreamScreen> createState() => _LiveStreamScreenState();
}
class _LiveStreamScreenState extends State<LiveStreamScreen> {
late Room _room;
// The local participant's mode, null until the room is joined.
Mode? _mode;
@override
void initState() {
super.initState();
// Create _room with VideoSDK.createRoom() before this point.
_room.on(Events.roomJoined, () {
setState(() => _mode = _room.localParticipant.mode);
});
// localParticipant.mode is already updated when this fires.
_room.on(Events.participantModeChanged, (Map<String, dynamic> data) {
if (data['participantId'] == _room.localParticipant.id) {
setState(() => _mode = _room.localParticipant.mode);
}
});
}
Future<void> _switchMode(Mode mode) async {
try {
if (mode == Mode.SEND_AND_RECV) {
// On Android, changeMode() does not ask for camera or microphone permission.
await VideoSDK.requestPermissions();
}
await _room.changeMode(mode);
} on VideoSDKError catch (error) {
debugPrint("changeMode failed: ${error.code} ${error.name}");
}
}
@override
Widget build(BuildContext context) {
return Column(children: [
ElevatedButton(
child: const Text('Change Mode'),
onPressed: () async {
if (_mode == Mode.SEND_AND_RECV) {
await _switchMode(Mode.RECV_ONLY); // change mode to RECV_ONLY
} else if (_mode == Mode.RECV_ONLY) {
await _switchMode(Mode.SEND_AND_RECV); // change Mode to SEND_AND_RECV
}
},
),
]);
}
}
Implementation Guide
Step 1 : Create a Pubsub Topic
- Set up a PubSub topic to send a mode change request from the host to a specific audience member.
Future<void> sendInvite(Room livestream, String participantId) async {
try {
await livestream.pubSub.publish(
"REQUEST_TO_JOIN_AS_HOST_$participantId", // PubSub topic specific to participant
"SEND_AND_RECV", // message
);
} on VideoSDKError catch (e) {
// The invite never reached the participant.
debugPrint("Invite failed: ${e.code} ${e.name}");
}
}
Step 2 : Create an Invite Button
- Add an "Invite" button for each audience member. When clicked, it publishes a PubSub message with the mode "SEND_AND_RECV" to that participant.
room.participantsholds the remote participants only, so the host is never in the list. Rebuild the list onparticipantModeChanged, as shown in Updating the UI on mode change.
Widget buildInviteList(Room livestream) {
// Remote participants only.
final participants = livestream.participants.values.toList();
return Expanded(
child: ListView.builder(
itemCount: participants.length,
itemBuilder: (context, index) => ParticipantListItem(
livestream: livestream,
participant: participants[index],
),
),
);
}
class ParticipantListItem extends StatelessWidget {
final Room livestream;
final Participant participant;
const ParticipantListItem({
super.key,
required this.livestream,
required this.participant,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(participant.displayName),
// Only audience members can be invited.
if (participant.mode == Mode.RECV_ONLY)
PopupMenuButton<String>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
icon: const Icon(Icons.more_vert),
onSelected: (value) async {
if (value == "Add as a Co-host") {
await sendInvite(livestream, participant.id);
}
},
itemBuilder: (context) => const [
PopupMenuItem<String>(
value: "Add as a Co-host",
child: Text("Add as a Co-host"),
),
],
),
],
),
);
}
}
Step 3 : Create a Listener to Change the Mode
- On the audience side, subscribe to the specific PubSub topic. When a mode request is received, update the participant’s mode using changeMode().
- Call
registerModeListener()from yourEvents.roomJoinedhandler. Asubscribe()call made before the room is joined throws.
Future<void> registerModeListener(Room livestream, BuildContext context) async {
try {
await livestream.pubSub.subscribe(
"REQUEST_TO_JOIN_AS_HOST_${livestream.localParticipant.id}",
PubSubMessageListener(onMessageReceived: (PubSubMessage pubSubMessage) {
final message = pubSubMessage.message;
if (message == "SEND_AND_RECV") {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text(
"${pubSubMessage.senderName} requested you to join as a speaker",
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Decline"),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
try {
// On Android, changeMode() does not ask for camera or microphone permission.
await VideoSDK.requestPermissions();
await livestream.changeMode(Mode.SEND_AND_RECV);
} on VideoSDKError catch (e) {
debugPrint("changeMode failed: ${e.code} ${e.name}");
}
},
child: const Text("Accept"),
),
],
),
);
}
}),
);
} on VideoSDKError catch (e) {
// Without this subscription the audience never sees an invite.
debugPrint("Subscribe failed: ${e.code} ${e.name}");
}
}
Calling changeMode() changes only the caller's mode. Every other client must rebuild its host and audience lists from the participantModeChanged event, as described in Updating the UI on mode change.
Updating the UI on mode change
changeMode() updates the mode of the participant who calls it. The room then broadcasts a participantModeChanged event to every participant, including clients that did not initiate the change. Each client should use that event to update its UI.
The event passes a Map with the participantId and the new mode as a Mode, such as Mode.SEND_AND_RECV. participant.mode is already updated when the event fires. In the event handler, rebuild the lists that drive your UI by filtering participants by their current mode, then refresh the view. Simply pinning, unpinning, or displaying a message is not enough: when a participant is promoted, their mode changes, but other clients will not automatically move them from the audience to the stage.
void setupModeListeners() {
// participant.mode is already updated when this fires.
room.on(Events.participantModeChanged, (Map<String, dynamic> data) {
rebuildLists();
});
room.on(Events.participantLeft, (Participant participant, LeaveReason reason) {
rebuildLists();
});
}
void rebuildLists() {
setState(() {
// Re-derive both lists on every mode change.
speakers = participantsWithMode(Mode.SEND_AND_RECV);
viewers = participantsWithMode(Mode.RECV_ONLY);
});
}
// room.participants holds remote participants only, so add yourself.
List<Participant> participantsWithMode(Mode mode) =>
[room.localParticipant, ...room.participants.values]
.where((participant) => participant.mode == mode)
.toList();
Rebuild both lists — the speakers and the viewers. Removing the participant from the audience list alone is the common half-fix: they disappear from the audience and never appear on stage.
API Reference
The API references for all the methods and events utilized in this guide are provided below.
Got a Question? Ask us on discord

