David Liu

speaker view

@@ -10,8 +10,11 @@ import androidx.appcompat.app.AppCompatActivity @@ -10,8 +10,11 @@ import androidx.appcompat.app.AppCompatActivity
10 import androidx.recyclerview.widget.LinearLayoutManager 10 import androidx.recyclerview.widget.LinearLayoutManager
11 import com.github.ajalt.timberkt.Timber 11 import com.github.ajalt.timberkt.Timber
12 import com.snakydesign.livedataextensions.combineLatest 12 import com.snakydesign.livedataextensions.combineLatest
  13 +import com.snakydesign.livedataextensions.scan
  14 +import com.snakydesign.livedataextensions.take
13 import com.xwray.groupie.GroupieAdapter 15 import com.xwray.groupie.GroupieAdapter
14 -import io.livekit.android.room.track.LocalVideoTrack 16 +import io.livekit.android.room.participant.Participant
  17 +import io.livekit.android.room.track.VideoTrack
15 import io.livekit.android.sample.databinding.CallActivityBinding 18 import io.livekit.android.sample.databinding.CallActivityBinding
16 import kotlinx.parcelize.Parcelize 19 import kotlinx.parcelize.Parcelize
17 20
@@ -68,15 +71,26 @@ class CallActivity : AppCompatActivity() { @@ -68,15 +71,26 @@ class CallActivity : AppCompatActivity() {
68 } 71 }
69 72
70 // speaker view setup 73 // speaker view setup
71 - viewModel.room.observe(this) { room -> 74 + viewModel.room.take(1).observe(this) { room ->
72 room.initVideoRenderer(binding.speakerView) 75 room.initVideoRenderer(binding.speakerView)
73 - val videoTrack = room.localParticipant.videoTracks.values  
74 - .firstOrNull()  
75 - ?.track as? LocalVideoTrack  
76 -  
77 - videoTrack?.let {  
78 - it.addRenderer(binding.speakerView)  
79 - } 76 + viewModel.activeSpeaker
  77 + .scan(Pair<Participant?, Participant?>(null, null)) { pair, participant ->
  78 + // old participant is first
  79 + // latest active participant is second
  80 + Pair(pair.second, participant)
  81 + }.observe(this) { (oldSpeaker, newSpeaker) ->
  82 + // Remove any renderering from the old speaker
  83 + oldSpeaker?.videoTracks
  84 + ?.values
  85 + ?.forEach { trackPublication ->
  86 + (trackPublication.track as? VideoTrack)?.removeRenderer(binding.speakerView)
  87 + }
  88 +
  89 + val videoTrack = newSpeaker?.videoTracks?.values
  90 + ?.firstOrNull()
  91 + ?.track as? VideoTrack
  92 + videoTrack?.addRenderer(binding.speakerView)
  93 + }
80 } 94 }
81 95
82 // Controls setup 96 // Controls setup
@@ -12,6 +12,7 @@ import io.livekit.android.events.RoomEvent @@ -12,6 +12,7 @@ import io.livekit.android.events.RoomEvent
12 import io.livekit.android.events.collect 12 import io.livekit.android.events.collect
13 import io.livekit.android.room.Room 13 import io.livekit.android.room.Room
14 import io.livekit.android.room.participant.Participant 14 import io.livekit.android.room.participant.Participant
  15 +import io.livekit.android.room.participant.RemoteParticipant
15 import io.livekit.android.room.track.CameraPosition 16 import io.livekit.android.room.track.CameraPosition
16 import io.livekit.android.room.track.LocalVideoTrack 17 import io.livekit.android.room.track.LocalVideoTrack
17 import io.livekit.android.room.track.Track 18 import io.livekit.android.room.track.Track
@@ -81,13 +82,18 @@ class CallViewModel( @@ -81,13 +82,18 @@ class CallViewModel(
81 } 82 }
82 83
83 private fun updateParticipants(room: Room) { 84 private fun updateParticipants(room: Room) {
84 - mutableParticipants.postValue(  
85 - listOf(room.localParticipant) +  
86 - room.remoteParticipants  
87 - .keys  
88 - .sortedBy { it }  
89 - .mapNotNull { room.remoteParticipants[it] }  
90 - ) 85 +
  86 + val participantList = listOf(room.localParticipant) +
  87 + room.remoteParticipants
  88 + .keys
  89 + .sortedBy { it }
  90 + .mapNotNull { room.remoteParticipants[it] }
  91 + mutableParticipants.postValue(participantList)
  92 +
  93 + if (!participantList.contains(mutableActiveSpeaker.value) || mutableActiveSpeaker.value == null) {
  94 + // active speaker has left, choose someone else at random.
  95 + mutableActiveSpeaker.postValue(participantList.last())
  96 + }
91 } 97 }
92 98
93 fun handleActiveSpeakersChanged(speakers: List<Participant>) { 99 fun handleActiveSpeakersChanged(speakers: List<Participant>) {
@@ -95,7 +101,9 @@ class CallViewModel( @@ -95,7 +101,9 @@ class CallViewModel(
95 if (speakers.isEmpty() || speakers.contains(mutableActiveSpeaker.value)) { 101 if (speakers.isEmpty() || speakers.contains(mutableActiveSpeaker.value)) {
96 return 102 return
97 } 103 }
98 - val newSpeaker = speakers.firstOrNull() ?: return 104 + val newSpeaker = speakers
  105 + .filter { it is RemoteParticipant } // Try not to display local participant as speaker.
  106 + .firstOrNull() ?: return
99 mutableActiveSpeaker.postValue(newSpeaker) 107 mutableActiveSpeaker.postValue(newSpeaker)
100 } 108 }
101 109