Invite Guest on Stage - Flutter
In this guide, we will see how you can request a viewer to join your livestream by using the changeMode().
Before going forward in this guide, do make sure all the attendees join the room with mode as SIGNALLING_ONLY and the host joins with mode as SEND_AND_RECV
Important Changes Flutter SDK in Version v1.3.0
- The following modes have been deprecated:
CONFERENCEhas been replaced bySEND_AND_RECVVIEWERhas been replaced bySIGNALLING_ONLY
Please update your implementation to use the new modes.
⚠️ Compatibility Notice:
To ensure a seamless meeting experience, all participants must use the same SDK version.
Do not mix version v1.3.0 + with older versions, as it may cause significant conflicts.
Let's first have a look at how we will be using the PubSub mechanism to acheive the requesting and switching of the participant's mode.

Step 1: Loading Viewer List
The host will see a list of participants who have joined as SIGNALLING_ONLY along with a button that, when selected, will invite that user to join the livestream.
room.participants holds the remote participants only, so the host is never in the viewer list. participant.mode is already updated when participantModeChanged fires, so the example rebuilds the list from that event.
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
class SpeakerScreen extends StatefulWidget {
...
}
class _SpeakerScreenState extends State<SpeakerScreen> {
late Room room;
Map<String, Participant> _attendees = {};
@override
void initState() {
...
updateAttendees();
setupRoomEventListener();
}
@override
Widget build(BuildContext context) {
return Column(
children:[
//button which will show viewers list
ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: ListView.builder(
itemCount: _attendees.values.length,
itemBuilder: (builderContext, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Text(_attendees.values
.elementAt(index)
.displayName)),
ElevatedButton(
onPressed: () {
},
child: Text("Invite on Stage")),
],
),
);
}),
));
},
child: const Text("Invite Viewer on Stage"),
),
...
]
);
}
void updateAttendees(){
Map<String, Participant> attendees = {};
room.participants.values.forEach((participant) {
if (participant.mode == Mode.SIGNALLING_ONLY) {
attendees.putIfAbsent(participant.id, () => participant);
}
});
setState(()=>_attendees = attendees);
}
void setupRoomEventListener() {
room.on(Events.roomJoined, () {
updateAttendees();
});
room.on(Events.participantModeChanged, (Map<String, dynamic> data){
updateAttendees();
});
room.on(Events.participantJoined, (Participant participant) {
updateAttendees();
});
room.on(Events.participantLeft, (Participant participant, LeaveReason reason) {
updateAttendees();
});
}
}
Step 2: Requesting a Viewer to Join Livestream
We have a Viewer list ready. Now, let us handle the click event for the Join Livestream button.
We will be using CHANGE_MODE_$participantId as the topic for PubSub.
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
class SpeakerScreen extends StatefulWidget {
...
}
class _SpeakerScreenState extends State<SpeakerScreen> {
late Room room;
Map<String, Participant> _attendees = {};
@override
void initState() {
...
}
@override
Widget build(BuildContext context) {
return Column(
children:[
ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: ListView.builder(
itemCount: _attendees.values.length,
itemBuilder: (builderContext, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Text(_attendees.values
.elementAt(index)
.displayName)),
ElevatedButton(
onPressed: () async {
//Publish the message on CHANGE_MODE_$participantId to join
try {
await room.pubSub.publish(
"CHANGE_MODE_${_attendees.values.elementAt(index).id}",
"SEND_AND_RECV");
} on VideoSDKError catch (error) {
log("Could not send the request: ${error.code} ${error.name}");
}
},
child: Text("Invite on Stage")),
],
),
);
}),
));
},
child: const Text("Invite Viewer on Stage"),
),
...
]
);
}
void updateAttendees(){
...
}
void setupRoomEventListener() {
...
}
}
Step 3: Showing Viewer Request Dialog
After implementing the Host requesting viewer to join the livestream, let's display the viewer's request dialogue and switch the SIGNALLING_ONLY mode to SEND_AND_RECV.
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:videosdk/videosdk.dart';
import './livestream_player.dart';
class ILSViewerView extends StatefulWidget {
final Room room;
...
}
class _ILSViewerViewState extends State<ILSViewerView> {
// Keep the listener: unsubscribe() takes the same object
late final PubSubMessageListener changeModeListener =
PubSubMessageListener(onMessageReceived: changeModeRequestHandler);
@override
void initState() {
super.initState();
//subscribe to the change mode request topic
subscribeToChangeModeRequests();
}
Future<void> subscribeToChangeModeRequests() async {
try {
await widget.room.pubSub.subscribe(
"CHANGE_MODE_${widget.room.localParticipant.id}",
changeModeListener);
} on VideoSDKError catch (error) {
log("Could not subscribe: ${error.code} ${error.name}");
}
}
//show dialog to accept or reject when change mode requested by speaker
void changeModeRequestHandler(PubSubMessage message) {
if (mounted) {
showDialog(
context: this.context,
builder: (context) {
return AlertDialog(
actions: [
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
try {
await widget.room.changeMode(Mode.SEND_AND_RECV);
} on VideoSDKError catch (error) {
log("Could not change mode: ${error.code} ${error.name}");
}
},
child: const Text("Accept"),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Reject"),
)
],
content:
Text("${message.senderName} requested you to join livestream"),
);
});
}
}
@override
Widget build(BuildContext context) {
return YourViewerWidget;
}
@override
void dispose() {
// The listener stops at once, even when this call fails
// because the room has already left.
widget.room.pubSub
.unsubscribe("CHANGE_MODE_${widget.room.localParticipant.id}",
changeModeListener)
.catchError((Object error) {});
super.dispose();
}
}
Step 4: Pin the participant
We need to pin the participant so that he/she can appears on the livestream. To achieve this, we will listen to the callback on the participantModeChanged of Room object. Here, we are already calling the updateAttendees() which we will update to pin only the SEND_AND_RECV mode participants.
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
class SpeakerScreen extends StatefulWidget {
...
}
class _SpeakerScreenState extends State<SpeakerScreen> {
late Room room;
bool _joined = false; // set to true in the roomJoined handler
Map<String, Participant> _attendees = {};
@override
void initState() {
...
}
@override
Widget build(BuildContext context) {
return Column(
children:[
...
]
);
}
void updateAttendees(){
Map<String, Participant> attendees = {};
room.participants.values.forEach((participant) {
if (participant.mode == Mode.SIGNALLING_ONLY) {
attendees.putIfAbsent(participant.id, () => participant);
setPinned(participant, false);
}
});
// room.participants holds remote participants only, so pin yourself here
// once the room is joined and your mode is SEND_AND_RECV.
if (_joined && room.localParticipant.mode == Mode.SEND_AND_RECV) {
setPinned(room.localParticipant, true);
}
setState(()=>_attendees = attendees);
}
Future<void> setPinned(Participant participant, bool pinned) async {
try {
if (pinned) {
await participant.pin();
} else {
await participant.unpin();
}
} on VideoSDKError catch (error) {
log("Could not update the pin: ${error.code} ${error.name}");
}
}
}
API Reference
The API references for all the methods utilized in this guide are provided below.
Got a Question? Ask us on discord

