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