Skip to main content
Version: 3.x.x

Rendering Host and Audience Views - Android

In a live stream setup, only hosts (participants in SEND_AND_RECV mode) can broadcast their audio and video. Audience members (in RECV_ONLY mode) are passive viewers who do not share their audio/video.

To ensure optimal performance and a clean user experience, your app should:

  • Render audio and video elements only for hosts (i.e., participants in SEND_AND_RECV mode).
  • Display the total audience count to give context on viewership without rendering individual audience tiles.

Filtering and Rendering Hosts​

The steps involved in rendering the audio and video of hosts are as follows.

  1. Filtering Hosts and Checking their Mic/Webcam Status
  2. Rendering Video Streams of Hosts
  3. Rendering Audio Streams of Hosts

1. Filtering Hosts and Checking their Mic/Webcam Status​

In a live stream, only participants in SEND_AND_RECV mode (i.e., hosts) actively share their audio and video. To render their streams, begin by accessing all participants using the Room class. Then, filter out only those in SEND_AND_RECV mode.

note

Rebuild these lists in the onParticipantModeChanged() event, so a promoted or demoted participant moves between the audience and the stage on every client.

private fun showParticipants() {
// Clear existing lists
speakerList?.clear()

// Filter for host participants (SEND_AND_RECV mode)
liveStream?.participants?.values?.forEach { participant ->
if (participant.mode == Mode.SEND_AND_RECV && speakerList!!.size < 4) {
speakerList?.add(participant)
}
}

// Add local participant if they're a host
if (liveStream?.localParticipant?.mode == Mode.SEND_AND_RECV && !speakerList!!.contains(liveStream?.localParticipant)) {
speakerList?.add(0, liveStream?.localParticipant!!)
}

// Update the UI with filtered participants
updateSpeakerGrid()
}

private fun updateSpeakerGrid() {
// Update UI based on number of speakers
when (speakerList?.size) {
1 -> {
speakerOneContainer?.visibility = View.VISIBLE
speakerTwoContainer?.visibility = View.GONE
speakerThreeContainer?.visibility = View.GONE
speakerFourContainer?.visibility = View.GONE

// Show first speaker
showInGUI(speakerList!![0], speakerOneView, speakerOneNameText)
}
2 -> {
speakerOneContainer?.visibility = View.VISIBLE
speakerTwoContainer?.visibility = View.VISIBLE
speakerThreeContainer?.visibility = View.GONE
speakerFourContainer?.visibility = View.GONE

// Show first and second speakers
showInGUI(speakerList!![0], speakerOneView, speakerOneNameText)
showInGUI(speakerList!![1], speakerTwoView, speakerTwoNameText)
}
}
}

2. Rendering Video Streams of Hosts​

Once you've filtered for participants in SEND_AND_RECV mode (i.e., hosts), you can use the Participant class to access their real-time data, including their streams and whether they are the local participant.

For each of these participants, use the Participant, which provides real-time information like displayName, and add a ParticipantEventListener: its onStreamEnabled() and onStreamDisabled() tell you when their microphone (MediaType.AUDIO stream) and webcam (MediaType.VIDEO stream) turn on and off. Display their name along with the current status of their microphone and webcam. If the webcam is off, show a simple placeholder with their name. If it's on, render their video feed. This ensures only hosts are visible to the audience, keeping the experience clean and intentional.

addEventListener() calls onStreamEnabled() right away for every stream the participant already has, so the tile starts with their current mic and webcam state. Because the list is rebuilt whenever a participant's mode changes, the example removes the listener a tile had for its previous participant before adding a new one.

import videosdk.org.webrtc.RendererCommon
import videosdk.org.webrtc.VideoTrack

// The participant and listener each tile is showing
private val tileListeners = mutableMapOf<VideoView, Pair<Participant, ParticipantEventListener>>()

private fun showInGUI(participant: Participant, videoView: VideoView, nameText: TextView) {
// Set participant name
nameText.text = if (participant.id == liveStream?.localParticipant?.id) "You" else participant.displayName

// Get references to status indicators from the tile that holds the VideoView
val tile = videoView.parent as View
val micStatusIcon = tile.findViewById<ImageView>(R.id.ivMicStatus)
val micStatusText = tile.findViewById<TextView>(R.id.tvMicStatus)
val webcamStatusIcon = tile.findViewById<ImageView>(R.id.ivWebcamStatus)
val webcamStatusText = tile.findViewById<TextView>(R.id.tvWebcamStatus)

// Detach the listener this tile had for its previous participant
tileListeners.remove(videoView)?.let { (previous, listener) -> previous.removeEventListener(listener) }

// Start with mic and webcam off; addEventListener() below turns on what is already streaming
updateMicStatus(false, micStatusIcon, micStatusText)
updateWebcamStatus(false, webcamStatusIcon, webcamStatusText, videoView)

// Add participant listener for status changes
val listener = object : ParticipantEventListener() {
override fun onStreamEnabled(stream: RTCStream) {
activity?.runOnUiThread {
if (stream.kind == MediaType.VIDEO) {
// Update webcam status
updateWebcamStatus(true, webcamStatusIcon, webcamStatusText, videoView)

// Set up video stream
videoView.visibility = View.VISIBLE
videoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)

// Mirror video for local participant
if (participant.id == liveStream?.localParticipant?.id) {
videoView.setMirror(true)
}

videoView.addTrack(stream.track as? VideoTrack)
} else if (stream.kind == MediaType.AUDIO) {
// Update mic status
updateMicStatus(true, micStatusIcon, micStatusText)
}
}
}

override fun onStreamDisabled(stream: RTCStream) {
activity?.runOnUiThread {
if (stream.kind == MediaType.VIDEO) {
// Update webcam status
updateWebcamStatus(false, webcamStatusIcon, webcamStatusText, videoView)

// Hide video view
videoView.visibility = View.GONE
} else if (stream.kind == MediaType.AUDIO) {
// Update mic status
updateMicStatus(false, micStatusIcon, micStatusText)
}
}
}
}
tileListeners[videoView] = participant to listener
participant.addEventListener(listener)
}

private fun updateMicStatus(isMicOn: Boolean, micStatusIcon: ImageView, micStatusText: TextView) {
micStatusIcon.setImageResource(if (isMicOn) R.drawable.ic_audio_on else R.drawable.ic_audio_off)
micStatusText.text = if (isMicOn) "On" else "Off"
}

private fun updateWebcamStatus(isWebcamOn: Boolean, webcamStatusIcon: ImageView, webcamStatusText: TextView, videoView: VideoView) {
webcamStatusIcon.setImageResource(if (isWebcamOn) R.drawable.ic_video_camera else R.drawable.ic_video_camera_off)
webcamStatusText.text = if (isWebcamOn) "On" else "Off"

// Show/hide video view based on webcam status
if (isWebcamOn) {
videoView.visibility = View.VISIBLE
} else {
videoView.removeTrack()
videoView.visibility = View.GONE
}
}

3. Rendering Audio Streams of Hosts​

Alongside video rendering, you can also play a participant’s audio whenever their mic is turned on. You don't render audio yourself: the SDK plays the audio of every participant it receives.

The example below lets a host turn their own microphone on and off with the suspend functions muteMic() and unmuteMic() of the Room class, and updates the audio output button when the audio device changes. The audio device listener is kept for the whole app and stays registered after leave(), so pass null to setAudioDeviceChangeListener() when your screen is destroyed.

private fun setupAudioControls() {
micButton?.setOnClickListener {
lifecycleScope.launch {
try {
if (liveStream?.isMicEnabled == true) {
// Turn off microphone
liveStream?.muteMic()
micButton?.setImageResource(R.drawable.ic_audio_off)
} else {
// Turn on microphone
liveStream?.unmuteMic()
micButton?.setImageResource(R.drawable.ic_audio_on)
}
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Mic toggle failed: ${e.code} ${e.message}")
}
}
}
}

private fun setupAudioDeviceListener() {
liveStream?.setAudioDeviceChangeListener { selectedAudioDevice, availableAudioDevices ->
// Handle audio device changes (e.g., switching between speaker, earpiece, headphones)
activity?.runOnUiThread {
// Update UI based on current audio device
when (selectedAudioDevice?.deviceId) {
"SPEAKER_PHONE" -> audioOutputButton?.setImageResource(R.drawable.ic_speaker)
"EARPIECE" -> audioOutputButton?.setImageResource(R.drawable.ic_earpiece)
"WIRED_HEADSET" -> audioOutputButton?.setImageResource(R.drawable.ic_headset)
"BLUETOOTH" -> audioOutputButton?.setImageResource(R.drawable.ic_bluetooth)
else -> Unit
}
}
}
}

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