Invite Guest on Stage - Android
In this guide, we will see how you can request a viewer to join your livestream by using changeMode().
Let's first have a look at how we will be using the PubSub mechanism to achieve the requesting and switching of the participant's mode.

Before going forward in this guide, do make sure all the attendees join the live stream with mode as Mode.SIGNALLING_ONLY and the host joins with mode as Mode.SEND_AND_RECV
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.
Here we are using RecyclerView to display the viewer list with a button.
This guide uses RecyclerView and the Material Components BottomSheetDialog. The SDK does not bring these libraries into your app, so add androidx.recyclerview:recyclerview and com.google.android.material:material to your app's dependencies.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#2B3034"
android:id="@+id/layout_participants">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingVertical="15sp"
android:text="Viewer List"
android:textSize="20sp"
android:textColor="@color/white"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvViewersLinearView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="15sp" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/viewerName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="20sp"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnRequest"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Request to join Stage"
android:textAllCaps="false" />
</LinearLayout>
- Create a recycler view adapter named
ViewerListAdapterwhich will show the viewer list. CreateViewHolderin the adapter which will extendRecyclerView.ViewHolder. The adapter also takes aCoroutineScope, such as the activity'slifecycleScope, for thesuspendPubSub call in Step 2.
class ViewerListAdapter(
items: ArrayList<Participant?>?,
private val room: Room,
private val scope: CoroutineScope
) : RecyclerView.Adapter<ViewerListAdapter.ViewHolder?>() {
private var viewers = ArrayList<Participant?>()
init {
viewers.addAll(items!!)
// adding Room Event listener to get the participant join/leave and participant mode change event in the room.
room.addEventListener(object : RoomEventListener() {
override fun onParticipantJoined(participant: Participant) {
super.onParticipantJoined(participant)
viewers = ArrayList()
viewers.addAll(allViewers)
notifyDataSetChanged()
}
override fun onParticipantLeft(participant: Participant, reason: LeaveReason) {
super.onParticipantLeft(participant, reason)
viewers = ArrayList()
viewers.addAll(allViewers)
notifyDataSetChanged()
}
override fun onParticipantModeChanged(participantId: String, mode: Mode) {
viewers = ArrayList()
viewers.addAll(allViewers)
notifyDataSetChanged()
}
})
}
private val allViewers: ArrayList<Participant?>
private get() {
val viewerList: ArrayList<Participant?> = ArrayList()
val participants: Iterator<Participant> = room.participants.values.iterator()
for (i in 0 until room.participants.size) {
val participant = participants.next()
if (participant.mode == Mode.SIGNALLING_ONLY) {
viewerList.add(participant)
}
}
return viewerList
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_viewer_list_layout, parent, false)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val participant = viewers[position]
holder.viewerName.text = viewers[position]!!.displayName
}
override fun getItemCount(): Int {
return viewers.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var viewerName: TextView
var btnRequest: Button
init {
viewerName = itemView.findViewById(R.id.viewerName)
btnRequest = itemView.findViewById(R.id.btnRequest)
}
}
}
- Set this adapter to
RecyclerView
btnViewerList.setOnClickListener { openViewerList() }
fun openViewerList() {
val viewerListView: RecyclerView
val bottomSheetDialog = BottomSheetDialog(mContext!!)
val layout_participants = findViewById<LinearLayout>(R.id.layout_participants)
val v3 = LayoutInflater.from(mContext)
.inflate(R.layout.layout_viewers_list_view, layout_participants)
bottomSheetDialog.setContentView(v3)
viewerListView = v3.findViewById(R.id.rvViewersLinearView)
viewerListView.minimumHeight = 400
bottomSheetDialog.show()
val viewerList = ArrayList<Participant?>()
val participants: Iterator<Participant> = room!!.participants.values.iterator()
for (i in 0 until room!!.participants.size) {
val participant = participants.next()
if (participant.mode == Mode.SIGNALLING_ONLY)
viewerList.add(participant)
}
viewerListView.layoutManager = LinearLayoutManager(mContext)
viewerListView.adapter = ViewerListAdapter(viewerList, room!!, lifecycleScope)
viewerListView.setHasFixedSize(true)
}

Step 2: Requesting a Viewer to Join Livestream
We have a Viewer list ready. Now, let us handle the click event for the btnRequest.
We will be using CHANGE_MODE_ + participantId as the topic for PubSub.
publish() is a suspend function, so call it from a coroutine. It returns once the server has the message, and throws a VideoSDKException when the live stream is not joined, is reconnecting, or the message could not be sent.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val participant = viewers[position]
//...
holder.btnRequest.setOnClickListener { v: View? ->
val pubSubPublishOptions = PubSubPublishOptions(persist = false)
scope.launch {
try {
room.pubSub.publish(
"CHANGE_MODE_" + participant!!.id,
participant.id,
pubSubPublishOptions
)
} catch (e: VideoSDKException) {
// Room is not joined yet, is reconnecting, or the message could not be sent
Log.d("VideoSDK", "publish failed: ${e.code} ${e.message}")
}
}
}
}
Step 3: Showing Viewer Request Dialog
Now that the host can request a viewer to join, show the request dialog on the viewer's side and switch the viewer from SIGNALLING_ONLY mode to SEND_AND_RECV.
- Subscribe to the topic from
onRoomJoined(), becausesubscribe()throws aVideoSDKExceptionwith3035before the live stream is joined.subscribe()is asuspendfunction, so launch a coroutine for it. - Keep the listener in a property.
onRoomJoined()also fires after a reconnect that needed a fresh join, and your subscription is kept, so subscribing the same listener again throws3073. Thecatchbelow handles it. changeMode()is asuspendfunction. Call it from a coroutine, such aslifecycleScope.launch(fromandroidx.lifecycle:lifecycle-runtime-ktx). It throws aVideoSDKExceptionwith3054when the participant is already in that mode, and3055while anotherchangeMode()is running.
private val changeModeListener = object : PubSubMessageListener {
override fun onMessageReceived(pubSubMessage: PubSubMessage) {
if (pubSubMessage.message == room!!.localParticipant.id) {
showRequestDialog(pubSubMessage.senderName ?: "")
}
}
}
room!!.addEventListener(object : RoomEventListener() {
override fun onRoomJoined(switchRoomId: String?) {
lifecycleScope.launch {
try {
room!!.pubSub.subscribe("CHANGE_MODE_" + room!!.localParticipant.id, changeModeListener)
} catch (e: VideoSDKException) {
// 3073: this listener is already subscribed, for example after a reconnect
Log.d("VideoSDK", "subscribe failed: ${e.code} ${e.message}")
}
}
}
})
private fun showRequestDialog(name: String) {
val builder = AlertDialog.Builder(
mContext!!
)
builder.setMessage("$name has requested you to go on stage")
builder.setTitle("Change mode Request")
builder.setCancelable(false)
builder.setPositiveButton("Yes") { dialog: DialogInterface, _: Int ->
lifecycleScope.launch {
try {
room!!.changeMode(Mode.SEND_AND_RECV)
//...
} catch (e: VideoSDKException) {
// 3054: already in this mode, 3055: another change is running
Log.e("VideoSDK", "changeMode failed: ${e.code} ${e.name}")
}
}
dialog.dismiss()
}
builder.setNegativeButton(
"No"
) { dialog: DialogInterface, which: Int -> dialog.dismiss() }
val alertDialog = builder.create()
alertDialog.show()
}
After changeMode(Mode.SEND_AND_RECV), the SDK itself turns on the mic and camera that the viewer enabled with micEnabled and camEnabled in createRoom(). Call unmuteMic() or enableCam() only for a device the viewer joined with turned off: for a device that is already on, they throw 3054. The viewer's app needs the RECORD_AUDIO and CAMERA permissions for either.

Step 4: Pin the participant
We need to pin the participant so that they appear on the livestream. To achieve this, we will listen to the callback on the onParticipantModeChanged of Room class.
pin() and unpin() are suspend functions, so the adapter takes a CoroutineScope to call them, such as the activity's lifecycleScope.
class ParticipantListAdapter(
items: ArrayList<Participant?>?,
private val room: Room,
private val context: Context,
private val scope: CoroutineScope
) : RecyclerView.Adapter<ParticipantListAdapter.ViewHolder>() {
// ...
init {
//...
room.addEventListener(object : RoomEventListener() {
override fun onParticipantJoined(participant: Participant) {
//...
}
override fun onParticipantLeft(participant: Participant, reason: LeaveReason) {
// ...
}
override fun onParticipantModeChanged(participantId: String, mode: Mode) {
//...
// room.participants holds only remote participants
val participant = room.participants[participantId] ?: return
scope.launch {
try {
if (mode == Mode.SEND_AND_RECV) {
participant.pin(PinType.SHARE_AND_CAM)
} else {
participant.unpin(PinType.SHARE_AND_CAM)
}
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "pin/unpin failed: ${e.code} ${e.name}")
}
}
}
})
}
//...
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
//...
}
}
API Reference
The API references for all the methods utilized in this guide are provided below.
Got a Question? Ask us on discord

