davidliu

Fix bug where primary speaker wasn't reset after they leave room

@@ -66,7 +66,7 @@ class LiveKit { @@ -66,7 +66,7 @@ class LiveKit {
66 token: String, 66 token: String,
67 options: ConnectOptions = ConnectOptions(), 67 options: ConnectOptions = ConnectOptions(),
68 roomOptions: RoomOptions = RoomOptions(), 68 roomOptions: RoomOptions = RoomOptions(),
69 - listener: RoomListener? 69 + listener: RoomListener? = null
70 ): Room { 70 ): Room {
71 val room = create(appContext, roomOptions) 71 val room = create(appContext, roomOptions)
72 72
@@ -9,7 +9,7 @@ import io.livekit.android.RoomOptions @@ -9,7 +9,7 @@ import io.livekit.android.RoomOptions
9 import io.livekit.android.events.RoomEvent 9 import io.livekit.android.events.RoomEvent
10 import io.livekit.android.events.collect 10 import io.livekit.android.events.collect
11 import io.livekit.android.room.Room 11 import io.livekit.android.room.Room
12 -import io.livekit.android.room.RoomListener 12 +import io.livekit.android.room.participant.LocalParticipant
13 import io.livekit.android.room.participant.Participant 13 import io.livekit.android.room.participant.Participant
14 import io.livekit.android.room.participant.RemoteParticipant 14 import io.livekit.android.room.participant.RemoteParticipant
15 import io.livekit.android.room.track.* 15 import io.livekit.android.room.track.*
@@ -23,7 +23,7 @@ class CallViewModel( @@ -23,7 +23,7 @@ class CallViewModel(
23 val url: String, 23 val url: String,
24 val token: String, 24 val token: String,
25 application: Application 25 application: Application
26 -) : AndroidViewModel(application), RoomListener { 26 +) : AndroidViewModel(application) {
27 private val mutableRoom = MutableStateFlow<Room?>(null) 27 private val mutableRoom = MutableStateFlow<Room?>(null)
28 val room: MutableStateFlow<Room?> = mutableRoom 28 val room: MutableStateFlow<Room?> = mutableRoom
29 val participants = mutableRoom.flatMapLatest { room -> 29 val participants = mutableRoom.flatMapLatest { room ->
@@ -84,8 +84,7 @@ class CallViewModel( @@ -84,8 +84,7 @@ class CallViewModel(
84 application, 84 application,
85 url, 85 url,
86 token, 86 token,
87 - roomOptions = RoomOptions(adaptiveStream = true),  
88 - listener = this@CallViewModel 87 + roomOptions = RoomOptions(adaptiveStream = true, dynacast = true),
89 ) 88 )
90 89
91 // Create and publish audio/video tracks 90 // Create and publish audio/video tracks
@@ -99,7 +98,18 @@ class CallViewModel( @@ -99,7 +98,18 @@ class CallViewModel(
99 98
100 mutablePrimarySpeaker.value = room.remoteParticipants.values.firstOrNull() ?: localParticipant 99 mutablePrimarySpeaker.value = room.remoteParticipants.values.firstOrNull() ?: localParticipant
101 100
102 - viewModelScope.launch { 101 + launch {
  102 + combine(participants, activeSpeakers) { participants, speakers -> participants to speakers }
  103 + .collect { (participantsList, speakers) ->
  104 + handlePrimarySpeaker(
  105 + participantsList,
  106 + speakers,
  107 + room
  108 + )
  109 + }
  110 + }
  111 +
  112 + launch {
103 room.events.collect { 113 room.events.collect {
104 when (it) { 114 when (it) {
105 is RoomEvent.FailedToConnect -> mutableError.value = it.error 115 is RoomEvent.FailedToConnect -> mutableError.value = it.error
@@ -117,6 +127,43 @@ class CallViewModel( @@ -117,6 +127,43 @@ class CallViewModel(
117 } 127 }
118 } 128 }
119 129
  130 + private fun handlePrimarySpeaker(participantsList: List<Participant>, speakers: List<Participant>, room: Room) {
  131 +
  132 + var speaker = mutablePrimarySpeaker.value
  133 +
  134 + // If speaker is local participant (due to defaults),
  135 + // attempt to find another remote speaker to replace with.
  136 + if (speaker is LocalParticipant) {
  137 + val remoteSpeaker = participantsList
  138 + .filterIsInstance<RemoteParticipant>() // Try not to display local participant as speaker.
  139 + .firstOrNull()
  140 +
  141 + if (remoteSpeaker != null) {
  142 + speaker = remoteSpeaker
  143 + }
  144 + }
  145 +
  146 + // If previous primary speaker leaves
  147 + if (!participantsList.contains(speaker)) {
  148 + // Default to another person in room, or local participant.
  149 + speaker = participantsList.filterIsInstance<RemoteParticipant>()
  150 + .firstOrNull()
  151 + ?: room.localParticipant
  152 + }
  153 +
  154 + if (speakers.isNotEmpty() && !speakers.contains(speaker)) {
  155 + val remoteSpeaker = speakers
  156 + .filterIsInstance<RemoteParticipant>() // Try not to display local participant as speaker.
  157 + .firstOrNull()
  158 +
  159 + if (remoteSpeaker != null) {
  160 + speaker = remoteSpeaker
  161 + }
  162 + }
  163 +
  164 + mutablePrimarySpeaker.value = speaker
  165 + }
  166 +
120 fun startScreenCapture(mediaProjectionPermissionResultData: Intent) { 167 fun startScreenCapture(mediaProjectionPermissionResultData: Intent) {
121 val localParticipant = room.value?.localParticipant ?: return 168 val localParticipant = room.value?.localParticipant ?: return
122 viewModelScope.launch { 169 viewModelScope.launch {
@@ -150,20 +197,6 @@ class CallViewModel( @@ -150,20 +197,6 @@ class CallViewModel(
150 mutableRoom.value?.disconnect() 197 mutableRoom.value?.disconnect()
151 } 198 }
152 199
153 - override fun onDisconnect(room: Room, error: Exception?) {  
154 - }  
155 -  
156 - override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {  
157 - // If old active speaker is still active, don't change.  
158 - if (speakers.isEmpty() || speakers.contains(mutablePrimarySpeaker.value)) {  
159 - return  
160 - }  
161 - val newSpeaker = speakers  
162 - .filter { it is RemoteParticipant } // Try not to display local participant as speaker.  
163 - .firstOrNull() ?: return  
164 - mutablePrimarySpeaker.value = newSpeaker  
165 - }  
166 -  
167 fun setMicEnabled(enabled: Boolean) { 200 fun setMicEnabled(enabled: Boolean) {
168 viewModelScope.launch { 201 viewModelScope.launch {
169 val localParticipant = room.value?.localParticipant ?: return@launch 202 val localParticipant = room.value?.localParticipant ?: return@launch