davidliu

Revert "Update samples (#102)"

This reverts commit 8642c61d.
@@ -82,7 +82,6 @@ class LiveKit { @@ -82,7 +82,6 @@ class LiveKit {
82 * @param url URL to LiveKit server (i.e. ws://mylivekitdeploy.io) 82 * @param url URL to LiveKit server (i.e. ws://mylivekitdeploy.io)
83 * @param listener Listener to Room events. LiveKit interactions take place with these callbacks 83 * @param listener Listener to Room events. LiveKit interactions take place with these callbacks
84 */ 84 */
85 - @Deprecated("Use LiveKit.create() and Room.connect() instead.")  
86 suspend fun connect( 85 suspend fun connect(
87 appContext: Context, 86 appContext: Context,
88 url: String, 87 url: String,
@@ -28,20 +28,22 @@ class CallViewModel( @@ -28,20 +28,22 @@ class CallViewModel(
28 val token: String, 28 val token: String,
29 application: Application 29 application: Application
30 ) : AndroidViewModel(application) { 30 ) : AndroidViewModel(application) {
31 -  
32 - val room = LiveKit.create(  
33 - appContext = application,  
34 - options = RoomOptions(adaptiveStream = true, dynacast = true),  
35 - )  
36 -  
37 - val participants = room::remoteParticipants.flow  
38 - .map { remoteParticipants ->  
39 - listOf<Participant>(room.localParticipant) +  
40 - remoteParticipants  
41 - .keys  
42 - .sortedBy { it }  
43 - .mapNotNull { remoteParticipants[it] } 31 + private val mutableRoom = MutableStateFlow<Room?>(null)
  32 + val room: MutableStateFlow<Room?> = mutableRoom
  33 + val participants = mutableRoom.flatMapLatest { room ->
  34 + if (room != null) {
  35 + room::remoteParticipants.flow
  36 + .map { remoteParticipants ->
  37 + listOf<Participant>(room.localParticipant) +
  38 + remoteParticipants
  39 + .keys
  40 + .sortedBy { it }
  41 + .mapNotNull { remoteParticipants[it] }
  42 + }
  43 + } else {
  44 + flowOf(emptyList())
44 } 45 }
  46 + }
45 47
46 private val mutableError = MutableStateFlow<Throwable?>(null) 48 private val mutableError = MutableStateFlow<Throwable?>(null)
47 val error = mutableError.hide() 49 val error = mutableError.hide()
@@ -49,7 +51,13 @@ class CallViewModel( @@ -49,7 +51,13 @@ class CallViewModel(
49 private val mutablePrimarySpeaker = MutableStateFlow<Participant?>(null) 51 private val mutablePrimarySpeaker = MutableStateFlow<Participant?>(null)
50 val primarySpeaker: StateFlow<Participant?> = mutablePrimarySpeaker 52 val primarySpeaker: StateFlow<Participant?> = mutablePrimarySpeaker
51 53
52 - val activeSpeakers = room::activeSpeakers.flow 54 + val activeSpeakers = mutableRoom.flatMapLatest { room ->
  55 + if (room != null) {
  56 + room::activeSpeakers.flow
  57 + } else {
  58 + flowOf(emptyList())
  59 + }
  60 + }
53 61
54 private var localScreencastTrack: LocalScreencastVideoTrack? = null 62 private var localScreencastTrack: LocalScreencastVideoTrack? = null
55 63
@@ -73,60 +81,60 @@ class CallViewModel( @@ -73,60 +81,60 @@ class CallViewModel(
73 81
74 init { 82 init {
75 viewModelScope.launch { 83 viewModelScope.launch {
  84 +
76 launch { 85 launch {
77 error.collect { Timber.e(it) } 86 error.collect { Timber.e(it) }
78 } 87 }
79 88
80 - launch {  
81 - combine(participants, activeSpeakers) { participants, speakers -> participants to speakers }  
82 - .collect { (participantsList, speakers) ->  
83 - handlePrimarySpeaker(  
84 - participantsList,  
85 - speakers,  
86 - room  
87 - )  
88 - }  
89 - } 89 + try {
  90 + val room = LiveKit.connect(
  91 + application,
  92 + url,
  93 + token,
  94 + roomOptions = RoomOptions(adaptiveStream = true, dynacast = true),
  95 + )
  96 +
  97 + // Create and publish audio/video tracks
  98 + val localParticipant = room.localParticipant
  99 + localParticipant.setMicrophoneEnabled(true)
  100 + mutableMicEnabled.postValue(localParticipant.isMicrophoneEnabled())
  101 +
  102 + localParticipant.setCameraEnabled(true)
  103 + mutableCameraEnabled.postValue(localParticipant.isCameraEnabled())
  104 + mutableRoom.value = room
  105 +
  106 + handlePrimarySpeaker(emptyList(), emptyList(), room)
  107 +
  108 + launch {
  109 + combine(participants, activeSpeakers) { participants, speakers -> participants to speakers }
  110 + .collect { (participantsList, speakers) ->
  111 + handlePrimarySpeaker(
  112 + participantsList,
  113 + speakers,
  114 + room
  115 + )
  116 + }
  117 + }
90 118
91 - launch {  
92 - room.events.collect {  
93 - when (it) {  
94 - is RoomEvent.FailedToConnect -> mutableError.value = it.error  
95 - is RoomEvent.DataReceived -> {  
96 - val identity = it.participant.identity ?: ""  
97 - val message = it.data.toString(Charsets.UTF_8)  
98 - mutableDataReceived.emit("$identity: $message") 119 + launch {
  120 + room.events.collect {
  121 + when (it) {
  122 + is RoomEvent.FailedToConnect -> mutableError.value = it.error
  123 + is RoomEvent.DataReceived -> {
  124 + val identity = it.participant.identity ?: ""
  125 + val message = it.data.toString(Charsets.UTF_8)
  126 + mutableDataReceived.emit("$identity: $message")
  127 + }
99 } 128 }
100 - else -> {}  
101 } 129 }
102 } 130 }
  131 + } catch (e: Throwable) {
  132 + mutableError.value = e
103 } 133 }
104 - connectToRoom()  
105 } 134 }
106 } 135 }
107 136
108 - private suspend fun connectToRoom() {  
109 - try {  
110 - room.connect(  
111 - url = url,  
112 - token = token,  
113 - )  
114 -  
115 - // Create and publish audio/video tracks  
116 - val localParticipant = room.localParticipant  
117 - localParticipant.setMicrophoneEnabled(true)  
118 - mutableMicEnabled.postValue(localParticipant.isMicrophoneEnabled())  
119 -  
120 - localParticipant.setCameraEnabled(true)  
121 - mutableCameraEnabled.postValue(localParticipant.isCameraEnabled())  
122 -  
123 - handlePrimarySpeaker(emptyList(), emptyList(), room)  
124 - } catch (e: Throwable) {  
125 - mutableError.value = e  
126 - }  
127 - }  
128 -  
129 - private fun handlePrimarySpeaker(participantsList: List<Participant>, speakers: List<Participant>, room: Room?) { 137 + private fun handlePrimarySpeaker(participantsList: List<Participant>, speakers: List<Participant>, room: Room) {
130 138
131 var speaker = mutablePrimarySpeaker.value 139 var speaker = mutablePrimarySpeaker.value
132 140
@@ -147,7 +155,7 @@ class CallViewModel( @@ -147,7 +155,7 @@ class CallViewModel(
147 // Default to another person in room, or local participant. 155 // Default to another person in room, or local participant.
148 speaker = participantsList.filterIsInstance<RemoteParticipant>() 156 speaker = participantsList.filterIsInstance<RemoteParticipant>()
149 .firstOrNull() 157 .firstOrNull()
150 - ?: room?.localParticipant 158 + ?: room.localParticipant
151 } 159 }
152 160
153 if (speakers.isNotEmpty() && !speakers.contains(speaker)) { 161 if (speakers.isNotEmpty() && !speakers.contains(speaker)) {
@@ -164,7 +172,7 @@ class CallViewModel( @@ -164,7 +172,7 @@ class CallViewModel(
164 } 172 }
165 173
166 fun startScreenCapture(mediaProjectionPermissionResultData: Intent) { 174 fun startScreenCapture(mediaProjectionPermissionResultData: Intent) {
167 - val localParticipant = room.localParticipant 175 + val localParticipant = room.value?.localParticipant ?: return
168 viewModelScope.launch { 176 viewModelScope.launch {
169 val screencastTrack = 177 val screencastTrack =
170 localParticipant.createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData) 178 localParticipant.createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
@@ -185,7 +193,7 @@ class CallViewModel( @@ -185,7 +193,7 @@ class CallViewModel(
185 viewModelScope.launch { 193 viewModelScope.launch {
186 localScreencastTrack?.let { localScreencastVideoTrack -> 194 localScreencastTrack?.let { localScreencastVideoTrack ->
187 localScreencastVideoTrack.stop() 195 localScreencastVideoTrack.stop()
188 - room.localParticipant.unpublishTrack(localScreencastVideoTrack) 196 + room.value?.localParticipant?.unpublishTrack(localScreencastVideoTrack)
189 mutableScreencastEnabled.postValue(localScreencastTrack?.enabled ?: false) 197 mutableScreencastEnabled.postValue(localScreencastTrack?.enabled ?: false)
190 } 198 }
191 } 199 }
@@ -193,35 +201,39 @@ class CallViewModel( @@ -193,35 +201,39 @@ class CallViewModel(
193 201
194 override fun onCleared() { 202 override fun onCleared() {
195 super.onCleared() 203 super.onCleared()
196 - room.disconnect() 204 + mutableRoom.value?.disconnect()
197 } 205 }
198 206
199 fun setMicEnabled(enabled: Boolean) { 207 fun setMicEnabled(enabled: Boolean) {
200 viewModelScope.launch { 208 viewModelScope.launch {
201 - room.localParticipant.setMicrophoneEnabled(enabled) 209 + val localParticipant = room.value?.localParticipant ?: return@launch
  210 + localParticipant.setMicrophoneEnabled(enabled)
202 mutableMicEnabled.postValue(enabled) 211 mutableMicEnabled.postValue(enabled)
203 } 212 }
204 } 213 }
205 214
206 fun setCameraEnabled(enabled: Boolean) { 215 fun setCameraEnabled(enabled: Boolean) {
207 viewModelScope.launch { 216 viewModelScope.launch {
208 - room.localParticipant.setCameraEnabled(enabled) 217 + val localParticipant = room.value?.localParticipant ?: return@launch
  218 + localParticipant.setCameraEnabled(enabled)
209 mutableCameraEnabled.postValue(enabled) 219 mutableCameraEnabled.postValue(enabled)
210 } 220 }
211 } 221 }
212 222
213 fun flipCamera() { 223 fun flipCamera() {
214 - val videoTrack = room.localParticipant.getTrackPublication(Track.Source.CAMERA)  
215 - ?.track as? LocalVideoTrack  
216 - ?: return  
217 -  
218 - val newOptions = when (videoTrack.options.position) {  
219 - CameraPosition.FRONT -> LocalVideoTrackOptions(position = CameraPosition.BACK)  
220 - CameraPosition.BACK -> LocalVideoTrackOptions(position = CameraPosition.FRONT)  
221 - else -> LocalVideoTrackOptions()  
222 - } 224 + room.value?.localParticipant?.let { participant ->
  225 + val videoTrack = participant.getTrackPublication(Track.Source.CAMERA)
  226 + ?.track as? LocalVideoTrack
  227 + ?: return@let
  228 +
  229 + val newOptions = when (videoTrack.options.position) {
  230 + CameraPosition.FRONT -> LocalVideoTrackOptions(position = CameraPosition.BACK)
  231 + CameraPosition.BACK -> LocalVideoTrackOptions(position = CameraPosition.FRONT)
  232 + else -> LocalVideoTrackOptions()
  233 + }
223 234
224 - videoTrack.restartTrack(newOptions) 235 + videoTrack.restartTrack(newOptions)
  236 + }
225 } 237 }
226 238
227 fun dismissError() { 239 fun dismissError() {
@@ -230,17 +242,17 @@ class CallViewModel( @@ -230,17 +242,17 @@ class CallViewModel(
230 242
231 fun sendData(message: String) { 243 fun sendData(message: String) {
232 viewModelScope.launch { 244 viewModelScope.launch {
233 - room.localParticipant.publishData(message.toByteArray(Charsets.UTF_8)) 245 + room.value?.localParticipant?.publishData(message.toByteArray(Charsets.UTF_8))
234 } 246 }
235 } 247 }
236 248
237 fun toggleSubscriptionPermissions() { 249 fun toggleSubscriptionPermissions() {
238 mutablePermissionAllowed.value = !mutablePermissionAllowed.value 250 mutablePermissionAllowed.value = !mutablePermissionAllowed.value
239 - room.localParticipant.setTrackSubscriptionPermissions(mutablePermissionAllowed.value) 251 + room.value?.localParticipant?.setTrackSubscriptionPermissions(mutablePermissionAllowed.value)
240 } 252 }
241 253
242 fun simulateMigration() { 254 fun simulateMigration() {
243 - room.sendSimulateScenario( 255 + room.value?.sendSimulateScenario(
244 LivekitRtc.SimulateScenario.newBuilder() 256 LivekitRtc.SimulateScenario.newBuilder()
245 .setMigration(true) 257 .setMigration(true)
246 .build() 258 .build()
@@ -249,14 +261,21 @@ class CallViewModel( @@ -249,14 +261,21 @@ class CallViewModel(
249 261
250 fun reconnect() { 262 fun reconnect() {
251 Timber.e { "Reconnecting." } 263 Timber.e { "Reconnecting." }
  264 + val room = mutableRoom.value ?: return
  265 + mutableRoom.value = null
252 mutablePrimarySpeaker.value = null 266 mutablePrimarySpeaker.value = null
253 room.disconnect() 267 room.disconnect()
254 viewModelScope.launch { 268 viewModelScope.launch {
255 - connectToRoom() 269 + room.connect(
  270 + url,
  271 + token
  272 + )
  273 + mutableRoom.value = room
256 } 274 }
257 } 275 }
258 } 276 }
259 277
260 private fun <T> LiveData<T>.hide(): LiveData<T> = this 278 private fun <T> LiveData<T>.hide(): LiveData<T> = this
  279 +
261 private fun <T> MutableStateFlow<T>.hide(): StateFlow<T> = this 280 private fun <T> MutableStateFlow<T>.hide(): StateFlow<T> = this
262 private fun <T> Flow<T>.hide(): Flow<T> = this 281 private fun <T> Flow<T>.hide(): Flow<T> = this
@@ -60,7 +60,7 @@ class CallActivity : AppCompatActivity() { @@ -60,7 +60,7 @@ class CallActivity : AppCompatActivity() {
60 60
61 // Setup compose view. 61 // Setup compose view.
62 setContent { 62 setContent {
63 - val room = viewModel.room 63 + val room by viewModel.room.collectAsState()
64 val participants by viewModel.participants.collectAsState(initial = emptyList()) 64 val participants by viewModel.participants.collectAsState(initial = emptyList())
65 val primarySpeaker by viewModel.primarySpeaker.collectAsState() 65 val primarySpeaker by viewModel.primarySpeaker.collectAsState()
66 val activeSpeakers by viewModel.activeSpeakers.collectAsState(initial = emptyList()) 66 val activeSpeakers by viewModel.activeSpeakers.collectAsState(initial = emptyList())
@@ -4,6 +4,7 @@ import android.app.Activity @@ -4,6 +4,7 @@ import android.app.Activity
4 import android.media.projection.MediaProjectionManager 4 import android.media.projection.MediaProjectionManager
5 import android.os.Bundle 5 import android.os.Bundle
6 import android.os.Parcelable 6 import android.os.Parcelable
  7 +import android.view.View
7 import android.widget.EditText 8 import android.widget.EditText
8 import android.widget.Toast 9 import android.widget.Toast
9 import androidx.activity.result.contract.ActivityResultContracts 10 import androidx.activity.result.contract.ActivityResultContracts
@@ -12,8 +13,11 @@ import androidx.appcompat.app.AppCompatActivity @@ -12,8 +13,11 @@ import androidx.appcompat.app.AppCompatActivity
12 import androidx.lifecycle.lifecycleScope 13 import androidx.lifecycle.lifecycleScope
13 import androidx.recyclerview.widget.LinearLayoutManager 14 import androidx.recyclerview.widget.LinearLayoutManager
14 import com.xwray.groupie.GroupieAdapter 15 import com.xwray.groupie.GroupieAdapter
  16 +import io.livekit.android.room.track.Track
  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.coroutines.flow.collectLatest 19 +import io.livekit.android.util.flow
  20 +import kotlinx.coroutines.flow.*
17 import kotlinx.parcelize.Parcelize 21 import kotlinx.parcelize.Parcelize
18 22
19 class CallActivity : AppCompatActivity() { 23 class CallActivity : AppCompatActivity() {
@@ -44,32 +48,87 @@ class CallActivity : AppCompatActivity() { @@ -44,32 +48,87 @@ class CallActivity : AppCompatActivity() {
44 setContentView(binding.root) 48 setContentView(binding.root)
45 49
46 // Audience row setup 50 // Audience row setup
47 - val audienceAdapter = GroupieAdapter() 51 + binding.audienceRow.layoutManager =
  52 + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
  53 + val adapter = GroupieAdapter()
  54 +
48 binding.audienceRow.apply { 55 binding.audienceRow.apply {
49 - layoutManager = LinearLayoutManager(this@CallActivity, LinearLayoutManager.HORIZONTAL, false)  
50 - adapter = audienceAdapter 56 + this.adapter = adapter
51 } 57 }
52 58
53 lifecycleScope.launchWhenCreated { 59 lifecycleScope.launchWhenCreated {
54 - viewModel.participants  
55 - .collect { participants ->  
56 - val items = participants.map { participant -> ParticipantItem(viewModel.room, participant) }  
57 - audienceAdapter.update(items) 60 + viewModel.room
  61 + .combine(viewModel.participants) { room, participants -> room to participants }
  62 + .collect { (room, participants) ->
  63 + if (room != null) {
  64 + val items = participants.map { participant -> ParticipantItem(room, participant) }
  65 + adapter.update(items)
  66 + }
58 } 67 }
59 } 68 }
60 69
61 // speaker view setup 70 // speaker view setup
62 - val speakerAdapter = GroupieAdapter()  
63 - binding.speakerView.apply {  
64 - layoutManager = LinearLayoutManager(this@CallActivity, LinearLayoutManager.HORIZONTAL, false)  
65 - adapter = speakerAdapter  
66 - }  
67 lifecycleScope.launchWhenCreated { 71 lifecycleScope.launchWhenCreated {
68 - viewModel.primarySpeaker.collectLatest { speaker ->  
69 - val items = listOfNotNull(speaker)  
70 - .map { participant -> ParticipantItem(viewModel.room, participant, speakerView = true) }  
71 - speakerAdapter.update(items)  
72 - } 72 + viewModel.room.filterNotNull().take(1)
  73 + .transform { room ->
  74 + // Initialize video renderer
  75 + room.initVideoRenderer(binding.speakerVideoView)
  76 +
  77 + // Observe primary speaker changes
  78 + emitAll(viewModel.primarySpeaker)
  79 + }.flatMapLatest { primarySpeaker ->
  80 + if (primarySpeaker != null) {
  81 + flowOf(primarySpeaker)
  82 + } else {
  83 + emptyFlow()
  84 + }
  85 + }.collect { participant ->
  86 + // Update new primary speaker identity
  87 + binding.identityText.text = participant.identity
  88 +
  89 + // observe videoTracks changes.
  90 + val videoTrackFlow = participant::videoTracks.flow
  91 + .map { participant to it }
  92 + .flatMapLatest { (participant, videoTracks) ->
  93 + // Prioritize any screenshare streams.
  94 + val trackPublication = participant.getTrackPublication(Track.Source.SCREEN_SHARE)
  95 + ?: participant.getTrackPublication(Track.Source.CAMERA)
  96 + ?: videoTracks.firstOrNull()?.first
  97 + ?: return@flatMapLatest emptyFlow()
  98 +
  99 + trackPublication::track.flow
  100 + }
  101 +
  102 + // observe audioTracks changes.
  103 + val mutedFlow = participant::audioTracks.flow
  104 + .flatMapLatest { tracks ->
  105 + val audioTrack = tracks.firstOrNull()?.first
  106 + if (audioTrack != null) {
  107 + audioTrack::muted.flow
  108 + } else {
  109 + flowOf(true)
  110 + }
  111 + }
  112 +
  113 + combine(videoTrackFlow, mutedFlow) { videoTrack, muted ->
  114 + videoTrack to muted
  115 + }.collect { (videoTrack, muted) ->
  116 + // Cleanup old video track
  117 + val oldVideoTrack = binding.speakerVideoView.tag as? VideoTrack
  118 + oldVideoTrack?.removeRenderer(binding.speakerVideoView)
  119 +
  120 + // Bind new video track to video view.
  121 + if (videoTrack is VideoTrack) {
  122 + videoTrack.addRenderer(binding.speakerVideoView)
  123 + binding.speakerVideoView.visibility = View.VISIBLE
  124 + } else {
  125 + binding.speakerVideoView.visibility = View.INVISIBLE
  126 + }
  127 + binding.speakerVideoView.tag = videoTrack
  128 +
  129 + binding.muteIndicator.visibility = if (muted) View.VISIBLE else View.INVISIBLE
  130 + }
  131 + }
73 } 132 }
74 133
75 // Controls setup 134 // Controls setup
@@ -145,9 +204,10 @@ class CallActivity : AppCompatActivity() { @@ -145,9 +204,10 @@ class CallActivity : AppCompatActivity() {
145 } 204 }
146 205
147 override fun onDestroy() { 206 override fun onDestroy() {
148 - binding.audienceRow.adapter = null  
149 - binding.speakerView.adapter = null  
150 super.onDestroy() 207 super.onDestroy()
  208 +
  209 + // Release video views
  210 + binding.speakerVideoView.release()
151 } 211 }
152 212
153 companion object { 213 companion object {
@@ -7,18 +7,20 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder @@ -7,18 +7,20 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder
7 import io.livekit.android.room.Room 7 import io.livekit.android.room.Room
8 import io.livekit.android.room.participant.ConnectionQuality 8 import io.livekit.android.room.participant.ConnectionQuality
9 import io.livekit.android.room.participant.Participant 9 import io.livekit.android.room.participant.Participant
  10 +import io.livekit.android.room.participant.ParticipantListener
  11 +import io.livekit.android.room.participant.RemoteParticipant
  12 +import io.livekit.android.room.track.RemoteTrackPublication
10 import io.livekit.android.room.track.Track 13 import io.livekit.android.room.track.Track
11 import io.livekit.android.room.track.VideoTrack 14 import io.livekit.android.room.track.VideoTrack
12 import io.livekit.android.sample.databinding.ParticipantItemBinding 15 import io.livekit.android.sample.databinding.ParticipantItemBinding
13 import io.livekit.android.util.flow 16 import io.livekit.android.util.flow
14 import kotlinx.coroutines.* 17 import kotlinx.coroutines.*
15 -import kotlinx.coroutines.flow.* 18 +import kotlinx.coroutines.flow.flatMapLatest
  19 +import kotlinx.coroutines.flow.flowOf
16 20
17 -@OptIn(ExperimentalCoroutinesApi::class)  
18 class ParticipantItem( 21 class ParticipantItem(
19 private val room: Room, 22 private val room: Room,
20 - private val participant: Participant,  
21 - private val speakerView: Boolean = false, 23 + private val participant: Participant
22 ) : BindableItem<ParticipantItemBinding>() { 24 ) : BindableItem<ParticipantItemBinding>() {
23 25
24 private var boundVideoTrack: VideoTrack? = null 26 private var boundVideoTrack: VideoTrack? = null
@@ -65,44 +67,25 @@ class ParticipantItem( @@ -65,44 +67,25 @@ class ParticipantItem(
65 if (quality == ConnectionQuality.POOR) View.VISIBLE else View.INVISIBLE 67 if (quality == ConnectionQuality.POOR) View.VISIBLE else View.INVISIBLE
66 } 68 }
67 } 69 }
68 -  
69 - // observe videoTracks changes.  
70 - val videoTrackPubFlow = participant::videoTracks.flow  
71 - .map { participant to it }  
72 - .flatMapLatest { (participant, videoTracks) ->  
73 - // Prioritize any screenshare streams.  
74 - val trackPublication = participant.getTrackPublication(Track.Source.SCREEN_SHARE)  
75 - ?: participant.getTrackPublication(Track.Source.CAMERA)  
76 - ?: videoTracks.firstOrNull()?.first  
77 -  
78 - flowOf(trackPublication) 70 + participant.listener = object : ParticipantListener {
  71 + override fun onTrackSubscribed(
  72 + track: Track,
  73 + publication: RemoteTrackPublication,
  74 + participant: RemoteParticipant
  75 + ) {
  76 + if (track !is VideoTrack) return
  77 + if (publication.source == Track.Source.CAMERA) {
  78 + setupVideoIfNeeded(track, viewBinding)
  79 + }
79 } 80 }
80 81
81 - coroutineScope?.launch {  
82 - videoTrackPubFlow  
83 - .flatMapLatest { pub ->  
84 - if (pub != null) {  
85 - pub::track.flow  
86 - } else {  
87 - flowOf(null)  
88 - }  
89 - }  
90 - .collectLatest { videoTrack ->  
91 - setupVideoIfNeeded(videoTrack as? VideoTrack, viewBinding)  
92 - }  
93 - }  
94 - coroutineScope?.launch {  
95 - videoTrackPubFlow  
96 - .flatMapLatest { pub ->  
97 - if (pub != null) {  
98 - pub::muted.flow  
99 - } else {  
100 - flowOf(true)  
101 - }  
102 - }  
103 - .collectLatest { muted ->  
104 - viewBinding.renderer.visibleOrInvisible(!muted)  
105 - } 82 + override fun onTrackUnpublished(
  83 + publication: RemoteTrackPublication,
  84 + participant: RemoteParticipant
  85 + ) {
  86 + super.onTrackUnpublished(publication, participant)
  87 + Timber.e { "Track unpublished" }
  88 + }
106 } 89 }
107 val existingTrack = getVideoTrack() 90 val existingTrack = getVideoTrack()
108 if (existingTrack != null) { 91 if (existingTrack != null) {
@@ -114,14 +97,14 @@ class ParticipantItem( @@ -114,14 +97,14 @@ class ParticipantItem(
114 return participant.getTrackPublication(Track.Source.CAMERA)?.track as? VideoTrack 97 return participant.getTrackPublication(Track.Source.CAMERA)?.track as? VideoTrack
115 } 98 }
116 99
117 - private fun setupVideoIfNeeded(videoTrack: VideoTrack?, viewBinding: ParticipantItemBinding) {  
118 - if (boundVideoTrack == videoTrack) { 100 + internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) {
  101 + if (boundVideoTrack != null) {
119 return 102 return
120 } 103 }
121 - boundVideoTrack?.removeRenderer(viewBinding.renderer) 104 +
122 boundVideoTrack = videoTrack 105 boundVideoTrack = videoTrack
123 Timber.v { "adding renderer to $videoTrack" } 106 Timber.v { "adding renderer to $videoTrack" }
124 - videoTrack?.addRenderer(viewBinding.renderer) 107 + videoTrack.addRenderer(viewBinding.renderer)
125 } 108 }
126 109
127 override fun unbind(viewHolder: GroupieViewHolder<ParticipantItemBinding>) { 110 override fun unbind(viewHolder: GroupieViewHolder<ParticipantItemBinding>) {
@@ -132,25 +115,5 @@ class ParticipantItem( @@ -132,25 +115,5 @@ class ParticipantItem(
132 boundVideoTrack = null 115 boundVideoTrack = null
133 } 116 }
134 117
135 - override fun getLayout(): Int =  
136 - if (speakerView)  
137 - R.layout.speaker_view  
138 - else  
139 - R.layout.participant_item  
140 -}  
141 -  
142 -private fun View.visibleOrGone(visible: Boolean) {  
143 - visibility = if (visible) {  
144 - View.VISIBLE  
145 - } else {  
146 - View.GONE  
147 - }  
148 -}  
149 -  
150 -private fun View.visibleOrInvisible(visible: Boolean) {  
151 - visibility = if (visible) {  
152 - View.VISIBLE  
153 - } else {  
154 - View.INVISIBLE  
155 - } 118 + override fun getLayout(): Int = R.layout.participant_item
156 } 119 }
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 android:layout_height="match_parent" 4 android:layout_height="match_parent"
5 android:keepScreenOn="true"> 5 android:keepScreenOn="true">
6 6
7 - <androidx.recyclerview.widget.RecyclerView 7 + <FrameLayout
8 android:id="@+id/speaker_view" 8 android:id="@+id/speaker_view"
9 android:layout_width="0dp" 9 android:layout_width="0dp"
10 android:layout_height="0dp" 10 android:layout_height="0dp"
@@ -14,7 +14,49 @@ @@ -14,7 +14,49 @@
14 app:layout_constraintStart_toStartOf="parent" 14 app:layout_constraintStart_toStartOf="parent"
15 app:layout_constraintTop_toTopOf="parent"> 15 app:layout_constraintTop_toTopOf="parent">
16 16
17 - </androidx.recyclerview.widget.RecyclerView> 17 + <ImageView
  18 + android:layout_width="120dp"
  19 + android:layout_height="120dp"
  20 + android:layout_gravity="center"
  21 + android:src="@drawable/outline_videocam_off_24"
  22 + app:tint="@color/no_video_participant" />
  23 +
  24 + <io.livekit.android.renderer.TextureViewRenderer
  25 + android:id="@+id/speaker_video_view"
  26 + android:layout_width="match_parent"
  27 + android:layout_height="match_parent" />
  28 + </FrameLayout>
  29 +
  30 + <FrameLayout
  31 + android:id="@+id/identity_bar"
  32 + android:layout_width="0dp"
  33 + android:layout_height="30dp"
  34 + android:background="#80000000"
  35 + app:layout_constraintBottom_toBottomOf="@id/speaker_view"
  36 + app:layout_constraintEnd_toEndOf="parent"
  37 + app:layout_constraintStart_toStartOf="parent" />
  38 +
  39 + <ImageView
  40 + android:id="@+id/mute_indicator"
  41 + android:layout_width="24dp"
  42 + android:layout_height="24dp"
  43 + android:layout_marginEnd="@dimen/identity_bar_padding"
  44 + android:src="@drawable/outline_mic_off_24"
  45 + app:layout_constraintBottom_toBottomOf="@id/identity_bar"
  46 + app:layout_constraintEnd_toEndOf="@id/identity_bar"
  47 + app:layout_constraintTop_toTopOf="@id/identity_bar"
  48 + app:tint="#BB0000" />
  49 +
  50 + <TextView
  51 + android:id="@+id/identity_text"
  52 + android:layout_width="0dp"
  53 + android:layout_height="wrap_content"
  54 + android:layout_marginStart="@dimen/identity_bar_padding"
  55 + android:ellipsize="end"
  56 + app:layout_constraintBottom_toBottomOf="@id/identity_bar"
  57 + app:layout_constraintEnd_toStartOf="@id/mute_indicator"
  58 + app:layout_constraintStart_toStartOf="@id/identity_bar"
  59 + app:layout_constraintTop_toTopOf="@id/identity_bar" />
18 60
19 <androidx.recyclerview.widget.RecyclerView 61 <androidx.recyclerview.widget.RecyclerView
20 android:id="@+id/audience_row" 62 android:id="@+id/audience_row"
1 -<?xml version="1.0" encoding="utf-8"?>  
2 -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"  
3 - xmlns:app="http://schemas.android.com/apk/res-auto"  
4 - android:layout_width="match_parent"  
5 - android:layout_height="match_parent"  
6 - android:background="@color/no_video_background">  
7 -  
8 - <FrameLayout  
9 - android:layout_width="match_parent"  
10 - android:layout_height="match_parent"  
11 - android:background="@color/no_video_background"  
12 - app:layout_constraintStart_toStartOf="parent"  
13 - app:layout_constraintTop_toTopOf="parent">  
14 -  
15 - <ImageView  
16 - android:layout_width="60dp"  
17 - android:layout_height="60dp"  
18 - android:layout_gravity="center"  
19 - android:src="@drawable/outline_videocam_off_24"  
20 - app:tint="@color/no_video_participant" />  
21 -  
22 - <io.livekit.android.renderer.TextureViewRenderer  
23 - android:id="@+id/renderer"  
24 - android:layout_width="match_parent"  
25 - android:layout_height="match_parent" />  
26 -  
27 - <ImageView  
28 - android:id="@+id/connection_quality"  
29 - android:layout_width="24dp"  
30 - android:layout_height="24dp"  
31 - android:layout_gravity="top|end"  
32 - android:layout_marginTop="@dimen/identity_bar_padding"  
33 - android:layout_marginEnd="@dimen/identity_bar_padding"  
34 - android:alpha="0.5"  
35 - android:src="@drawable/wifi_strength_1"  
36 - android:visibility="invisible"  
37 - app:tint="#FF0000" />  
38 - </FrameLayout>  
39 -  
40 - <FrameLayout  
41 - android:id="@+id/identity_bar"  
42 - android:layout_width="0dp"  
43 - android:layout_height="30dp"  
44 - android:background="#80000000"  
45 - app:layout_constraintBottom_toBottomOf="parent"  
46 - app:layout_constraintEnd_toEndOf="parent"  
47 - app:layout_constraintStart_toStartOf="parent" />  
48 -  
49 - <ImageView  
50 - android:id="@+id/mute_indicator"  
51 - android:layout_width="24dp"  
52 - android:layout_height="24dp"  
53 - android:layout_marginEnd="@dimen/identity_bar_padding"  
54 - android:src="@drawable/outline_mic_off_24"  
55 - android:visibility="gone"  
56 - app:layout_constraintBottom_toBottomOf="@id/identity_bar"  
57 - app:layout_constraintEnd_toEndOf="@id/identity_bar"  
58 - app:layout_constraintTop_toTopOf="@id/identity_bar"  
59 - app:tint="#BB0000" />  
60 -  
61 - <TextView  
62 - android:id="@+id/identity_text"  
63 - android:layout_width="0dp"  
64 - android:layout_height="wrap_content"  
65 - android:layout_marginStart="@dimen/identity_bar_padding"  
66 - android:ellipsize="end"  
67 - app:layout_constraintBottom_toBottomOf="@id/identity_bar"  
68 - app:layout_constraintEnd_toStartOf="@id/mute_indicator"  
69 - app:layout_constraintStart_toStartOf="@id/identity_bar"  
70 - app:layout_constraintTop_toTopOf="@id/identity_bar" />  
71 -</androidx.constraintlayout.widget.ConstraintLayout>