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.
- Filtering Hosts and Checking their Mic/Webcam Status
- Rendering Video Streams of Hosts
- 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 Meeting
class. Then, filter out only those in SEND_AND_RECV
mode.
- Kotlin
- Java
private fun showParticipants() {
// Clear existing lists
speakerList?.clear()
// Filter for host participants (SEND_AND_RECV mode)
liveStream?.participants?.values?.forEach { participant ->
if (participant.mode == "SEND_AND_RECV" && speakerList!!.size < 4) {
speakerList?.add(participant)
}
}
// Add local participant if they're a host
if (liveStream?.localParticipant?.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)
}
}
}
private void showParticipants() {
// Clear existing lists
if (speakerList != null) {
speakerList.clear();
}
// Filter for host participants (SEND_AND_RECV mode)
if (liveStream != null && liveStream.getParticipants() != null) {
for (Participant participant : liveStream.getParticipants().values()) {
if ("SEND_AND_RECV".equals(participant.getMode()) && speakerList.size() < 4) {
speakerList.add(participant);
}
}
}
// Add local participant if they're a host
if (liveStream != null && "SEND_AND_RECV".equals(liveStream.getLocalParticipant().getMode())
&& !speakerList.contains(liveStream.getLocalParticipant())) {
speakerList.add(0, liveStream.getLocalParticipant());
}
// Update the UI with filtered participants
updateSpeakerGrid();
}
private void updateSpeakerGrid() {
// Update UI based on number of speakers
if (speakerList == null) return;
switch (speakerList.size()) {
case 1:
speakerOneContainer.setVisibility(View.VISIBLE);
speakerTwoContainer.setVisibility(View.GONE);
speakerThreeContainer.setVisibility(View.GONE);
speakerFourContainer.setVisibility(View.GONE);
// Show first speaker
showInGUI(speakerList.get(0), speakerOneView, speakerOneNameText);
break;
case 2:
speakerOneContainer.setVisibility(View.VISIBLE);
speakerTwoContainer.setVisibility(View.VISIBLE);
speakerThreeContainer.setVisibility(View.GONE);
speakerFourContainer.setVisibility(View.GONE);
// Show first and second speakers
showInGUI(speakerList.get(0), speakerOneView, speakerOneNameText);
showInGUI(speakerList.get(1), speakerTwoView, speakerTwoNameText);
break;
}
}
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 webcamStream, webcamOn, and whether they are the local participant.
For each of these participants, use the Participant
, which provides real-time information like displayName, micOn, and webcamOn. 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.
- Kotlin
- Java
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
val micStatusIcon = videoView.findViewById<ImageView>(R.id.ivMicStatus)
val micStatusText = videoView.findViewById<TextView>(R.id.tvMicStatus)
val webcamStatusIcon = videoView.findViewById<ImageView>(R.id.ivWebcamStatus)
val webcamStatusText = videoView.findViewById<TextView>(R.id.tvWebcamStatus)
// Set initial status
updateMicStatus(participant.isMicOn, micStatusIcon, micStatusText)
updateWebcamStatus(participant.isWebcamOn, webcamStatusIcon, webcamStatusText, videoView)
// Add participant listener for status changes
participant.addEventListener(object : ParticipantEventListener() {
override fun onStreamEnabled(stream: Stream) {
activity?.runOnUiThread {
if (stream.kind.equals("video", ignoreCase = true)) {
// 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.mediaStream, stream.trackId)
} else if (stream.kind.equals("audio", ignoreCase = true)) {
// Update mic status
updateMicStatus(true, micStatusIcon, micStatusText)
}
}
}
override fun onStreamDisabled(stream: Stream) {
activity?.runOnUiThread {
if (stream.kind.equals("video", ignoreCase = true)) {
// Update webcam status
updateWebcamStatus(false, webcamStatusIcon, webcamStatusText, videoView)
// Hide video view
videoView.visibility = View.GONE
} else if (stream.kind.equals("audio", ignoreCase = true)) {
// Update mic status
updateMicStatus(false, micStatusIcon, micStatusText)
}
}
}
})
// Set up initial streams
participant.streams.values.forEach { stream ->
if (stream.kind.equals("video", ignoreCase = true)) {
// 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.mediaStream, stream.trackId)
}
}
}
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
}
}
private void showInGUI(Participant participant, VideoView videoView, TextView nameText) {
// Set participant name
if (participant.getId().equals(liveStream.getLocalParticipant().getId())) {
nameText.setText("You");
} else {
nameText.setText(participant.getDisplayName());
}
// Get references to status indicators
ImageView micStatusIcon = videoView.findViewById(R.id.ivMicStatus);
TextView micStatusText = videoView.findViewById(R.id.tvMicStatus);
ImageView webcamStatusIcon = videoView.findViewById(R.id.ivWebcamStatus);
TextView webcamStatusText = videoView.findViewById(R.id.tvWebcamStatus);
// Set initial status
updateMicStatus(participant.isMicOn(), micStatusIcon, micStatusText);
updateWebcamStatus(participant.isWebcamOn(), webcamStatusIcon, webcamStatusText, videoView);
// Add participant listener for status changes
participant.addEventListener(new ParticipantEventListener() {
@Override
public void onStreamEnabled(Stream stream) {
if (activity != null) {
activity.runOnUiThread(() -> {
if ("video".equalsIgnoreCase(stream.getKind())) {
// Update webcam status
updateWebcamStatus(true, webcamStatusIcon, webcamStatusText, videoView);
// Set up video stream
videoView.setVisibility(View.VISIBLE);
videoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
// Mirror video for local participant
if (participant.getId().equals(liveStream.getLocalParticipant().getId())) {
videoView.setMirror(true);
}
videoView.addTrack(stream.getMediaStream(), stream.getTrackId());
} else if ("audio".equalsIgnoreCase(stream.getKind())) {
// Update mic status
updateMicStatus(true, micStatusIcon, micStatusText);
}
});
}
}
@Override
public void onStreamDisabled(Stream stream) {
if (activity != null) {
activity.runOnUiThread(() -> {
if ("video".equalsIgnoreCase(stream.getKind())) {
// Update webcam status
updateWebcamStatus(false, webcamStatusIcon, webcamStatusText, videoView);
// Hide video view
videoView.setVisibility(View.GONE);
} else if ("audio".equalsIgnoreCase(stream.getKind())) {
// Update mic status
updateMicStatus(false, micStatusIcon, micStatusText);
}
});
}
}
});
// Set up initial streams
for (Stream stream : participant.getStreams().values()) {
if ("video".equalsIgnoreCase(stream.getKind())) {
// Set up video stream
videoView.setVisibility(View.VISIBLE);
videoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
// Mirror video for local participant
if (participant.getId().equals(liveStream.getLocalParticipant().getId())) {
videoView.setMirror(true);
}
videoView.addTrack(stream.getMediaStream(), stream.getTrackId());
}
}
}
private void updateMicStatus(boolean isMicOn, ImageView micStatusIcon, TextView micStatusText) {
micStatusIcon.setImageResource(isMicOn ? R.drawable.ic_audio_on : R.drawable.ic_audio_off);
micStatusText.setText(isMicOn ? "On" : "Off");
}
private void updateWebcamStatus(boolean isWebcamOn, ImageView webcamStatusIcon, TextView webcamStatusText, VideoView videoView) {
webcamStatusIcon.setImageResource(isWebcamOn ? R.drawable.ic_video_camera : R.drawable.ic_video_camera_off);
webcamStatusText.setText(isWebcamOn ? "On" : "Off");
// Show/hide video view based on webcam status
if (isWebcamOn) {
videoView.setVisibility(View.VISIBLE);
} else {
videoView.removeTrack();
videoView.setVisibility(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.
- Kotlin
- Java
private fun setupAudioControls() {
micButton?.setOnClickListener {
if (liveStream?.localParticipant?.isMicOn == true) {
// Turn off microphone
liveStream?.localParticipant?.disableMic()
micButton?.setImageResource(R.drawable.ic_audio_off)
} else {
// Turn on microphone
liveStream?.localParticipant?.enableMic()
micButton?.setImageResource(R.drawable.ic_audio_on)
}
}
}
liveStream?.addEventListener(object : MeetingEventListener() {
override fun onAudioDeviceChanged(audioDevice: String, availableAudioDevices: Set<String>) {
// Handle audio device changes (e.g., switching between speaker, earpiece, headphones)
activity?.runOnUiThread {
// Update UI based on current audio device
when (audioDevice) {
"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)
}
}
}
})
private void setupAudioControls() {
if (micButton != null) {
micButton.setOnClickListener(v -> {
if (liveStream != null && liveStream.getLocalParticipant() != null) {
if (liveStream.getLocalParticipant().isMicOn()) {
// Turn off microphone
liveStream.getLocalParticipant().disableMic();
micButton.setImageResource(R.drawable.ic_audio_off);
} else {
// Turn on microphone
liveStream.getLocalParticipant().enableMic();
micButton.setImageResource(R.drawable.ic_audio_on);
}
}
});
}
}
private void setupAudioDeviceListener() {
if (liveStream != null) {
liveStream.addEventListener(new MeetingEventListener() {
@Override
public void onAudioDeviceChanged(String audioDevice, Set<String> availableAudioDevices) {
// Handle audio device changes (e.g., switching between speaker, earpiece, headphones)
activity.runOnUiThread(() -> {
// Update UI based on current audio device
switch (audioDevice) {
case "SPEAKER_PHONE":
if (audioOutputButton != null)
audioOutputButton.setImageResource(R.drawable.ic_speaker);
break;
case "EARPIECE":
if (audioOutputButton != null)
audioOutputButton.setImageResource(R.drawable.ic_earpiece);
break;
case "WIRED_HEADSET":
if (audioOutputButton != null)
audioOutputButton.setImageResource(R.drawable.ic_headset);
break;
case "BLUETOOTH":
if (audioOutputButton != null)
audioOutputButton.setImageResource(R.drawable.ic_bluetooth);
break;
}
});
}
});
}
}
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