davidliu
Committed by GitHub

Properly emit upon track subscription for audio/video flows (#84)

* upgrade gradle

* Properly emit upon track subscription for audio/video flows

* Move VideoItem into SDK
@@ -2,8 +2,8 @@ @@ -2,8 +2,8 @@
2 2
3 buildscript { 3 buildscript {
4 ext { 4 ext {
5 - compose_version = '1.1.0-rc01'  
6 - compose_compiler_version = '1.1.0-rc02' 5 + compose_version = '1.1.1'
  6 + compose_compiler_version = '1.1.1'
7 kotlin_version = '1.6.10' 7 kotlin_version = '1.6.10'
8 java_version = JavaVersion.VERSION_1_8 8 java_version = JavaVersion.VERSION_1_8
9 dokka_version = '1.5.0' 9 dokka_version = '1.5.0'
@@ -14,7 +14,7 @@ buildscript { @@ -14,7 +14,7 @@ buildscript {
14 jcenter() 14 jcenter()
15 } 15 }
16 dependencies { 16 dependencies {
17 - classpath 'com.android.tools.build:gradle:7.0.4' 17 + classpath 'com.android.tools.build:gradle:7.1.2'
18 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 18 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
19 classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 19 classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
20 classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" 20 classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
1 #Thu Apr 29 14:50:17 JST 2021 1 #Thu Apr 29 14:50:17 JST 2021
2 distributionBase=GRADLE_USER_HOME 2 distributionBase=GRADLE_USER_HOME
3 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 3 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 distributionPath=wrapper/dists 4 distributionPath=wrapper/dists
5 zipStorePath=wrapper/dists 5 zipStorePath=wrapper/dists
6 zipStoreBase=GRADLE_USER_HOME 6 zipStoreBase=GRADLE_USER_HOME
@@ -42,6 +42,12 @@ android { @@ -42,6 +42,12 @@ android {
42 sourceCompatibility java_version 42 sourceCompatibility java_version
43 targetCompatibility java_version 43 targetCompatibility java_version
44 } 44 }
  45 + buildFeatures {
  46 + compose true
  47 + }
  48 + composeOptions {
  49 + kotlinCompilerExtensionVersion compose_compiler_version
  50 + }
45 kotlinOptions { 51 kotlinOptions {
46 freeCompilerArgs = ["-Xinline-classes", "-Xopt-in=kotlin.RequiresOptIn"] 52 freeCompilerArgs = ["-Xinline-classes", "-Xopt-in=kotlin.RequiresOptIn"]
47 jvmTarget = java_version 53 jvmTarget = java_version
  1 +package io.livekit.android.compose
  2 +
  3 +import androidx.compose.runtime.*
  4 +import androidx.compose.ui.Modifier
  5 +import androidx.compose.ui.layout.onGloballyPositioned
  6 +import androidx.compose.ui.viewinterop.AndroidView
  7 +import io.livekit.android.renderer.TextureViewRenderer
  8 +import io.livekit.android.room.Room
  9 +import io.livekit.android.room.track.RemoteVideoTrack
  10 +import io.livekit.android.room.track.VideoTrack
  11 +import io.livekit.android.room.track.video.ComposeVisibility
  12 +
  13 +/**
  14 + * Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use
  15 + * [TextureViewRenderer].
  16 + */
  17 +@Composable
  18 +fun VideoRenderer(
  19 + room: Room,
  20 + videoTrack: VideoTrack,
  21 + modifier: Modifier = Modifier
  22 +) {
  23 +
  24 + val videoSinkVisibility = remember(room, videoTrack) { ComposeVisibility() }
  25 + var boundVideoTrack by remember { mutableStateOf<VideoTrack?>(null) }
  26 + var view: TextureViewRenderer? by remember { mutableStateOf(null) }
  27 +
  28 + fun cleanupVideoTrack() {
  29 + view?.let { boundVideoTrack?.removeRenderer(it) }
  30 + boundVideoTrack = null
  31 + }
  32 +
  33 + fun setupVideoIfNeeded(videoTrack: VideoTrack, view: TextureViewRenderer) {
  34 + if (boundVideoTrack == videoTrack) {
  35 + return
  36 + }
  37 +
  38 + cleanupVideoTrack()
  39 +
  40 + boundVideoTrack = videoTrack
  41 + if (videoTrack is RemoteVideoTrack) {
  42 + videoTrack.addRenderer(view, videoSinkVisibility)
  43 + } else {
  44 + videoTrack.addRenderer(view)
  45 + }
  46 + }
  47 +
  48 + DisposableEffect(room, videoTrack) {
  49 + onDispose {
  50 + videoSinkVisibility.onDispose()
  51 + cleanupVideoTrack()
  52 + }
  53 + }
  54 +
  55 + DisposableEffect(currentCompositeKeyHash.toString()) {
  56 + onDispose {
  57 + view?.release()
  58 + }
  59 + }
  60 +
  61 + AndroidView(
  62 + factory = { context ->
  63 + TextureViewRenderer(context).apply {
  64 + room.initVideoRenderer(this)
  65 + setupVideoIfNeeded(videoTrack, this)
  66 +
  67 + view = this
  68 + }
  69 + },
  70 + update = { v -> setupVideoIfNeeded(videoTrack, v) },
  71 + modifier = modifier
  72 + .onGloballyPositioned { videoSinkVisibility.onGloballyPositioned(it) },
  73 + )
  74 +}
@@ -9,17 +9,13 @@ import io.livekit.android.room.track.RemoteTrackPublication @@ -9,17 +9,13 @@ import io.livekit.android.room.track.RemoteTrackPublication
9 import io.livekit.android.room.track.Track 9 import io.livekit.android.room.track.Track
10 import io.livekit.android.room.track.TrackPublication 10 import io.livekit.android.room.track.TrackPublication
11 import io.livekit.android.util.FlowObservable 11 import io.livekit.android.util.FlowObservable
12 -import io.livekit.android.util.LKLog  
13 import io.livekit.android.util.flow 12 import io.livekit.android.util.flow
14 import io.livekit.android.util.flowDelegate 13 import io.livekit.android.util.flowDelegate
15 import kotlinx.coroutines.CoroutineDispatcher 14 import kotlinx.coroutines.CoroutineDispatcher
16 import kotlinx.coroutines.CoroutineScope 15 import kotlinx.coroutines.CoroutineScope
17 import kotlinx.coroutines.SupervisorJob 16 import kotlinx.coroutines.SupervisorJob
18 -import kotlinx.coroutines.flow.SharingStarted  
19 -import kotlinx.coroutines.flow.map  
20 -import kotlinx.coroutines.flow.stateIn 17 +import kotlinx.coroutines.flow.*
21 import livekit.LivekitModels 18 import livekit.LivekitModels
22 -import livekit.LivekitRtc  
23 import javax.inject.Named 19 import javax.inject.Named
24 20
25 open class Participant( 21 open class Participant(
@@ -139,6 +135,21 @@ open class Participant( @@ -139,6 +135,21 @@ open class Participant(
139 var tracks by flowDelegate(emptyMap<String, TrackPublication>()) 135 var tracks by flowDelegate(emptyMap<String, TrackPublication>())
140 protected set 136 protected set
141 137
  138 + private fun Flow<Map<String, TrackPublication>>.trackUpdateFlow(): Flow<List<Pair<TrackPublication, Track?>>> {
  139 + return flatMapLatest { videoTracks ->
  140 + combine(
  141 + videoTracks.values
  142 + .map { trackPublication ->
  143 + // Re-emit when track changes
  144 + trackPublication::track.flow
  145 + .map { trackPublication to trackPublication.track }
  146 + }
  147 + ) { trackPubs ->
  148 + trackPubs.toList()
  149 + }
  150 + }
  151 + }
  152 +
142 /** 153 /**
143 * Changes can be observed by using [io.livekit.android.util.flow] 154 * Changes can be observed by using [io.livekit.android.util.flow]
144 */ 155 */
@@ -147,7 +158,8 @@ open class Participant( @@ -147,7 +158,8 @@ open class Participant(
147 val audioTracks by flowDelegate( 158 val audioTracks by flowDelegate(
148 stateFlow = ::tracks.flow 159 stateFlow = ::tracks.flow
149 .map { it.filterValues { publication -> publication.kind == Track.Kind.AUDIO } } 160 .map { it.filterValues { publication -> publication.kind == Track.Kind.AUDIO } }
150 - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) 161 + .trackUpdateFlow()
  162 + .stateIn(scope, SharingStarted.Eagerly, emptyList())
151 ) 163 )
152 164
153 /** 165 /**
@@ -157,10 +169,9 @@ open class Participant( @@ -157,10 +169,9 @@ open class Participant(
157 @get:FlowObservable 169 @get:FlowObservable
158 val videoTracks by flowDelegate( 170 val videoTracks by flowDelegate(
159 stateFlow = ::tracks.flow 171 stateFlow = ::tracks.flow
160 - .map {  
161 - it.filterValues { publication -> publication.kind == Track.Kind.VIDEO }  
162 - }  
163 - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) 172 + .map { it.filterValues { publication -> publication.kind == Track.Kind.VIDEO } }
  173 + .trackUpdateFlow()
  174 + .stateIn(scope, SharingStarted.Eagerly, emptyList())
164 ) 175 )
165 176
166 /** 177 /**
@@ -3,6 +3,7 @@ package io.livekit.android.room.track @@ -3,6 +3,7 @@ package io.livekit.android.room.track
3 import android.view.View 3 import android.view.View
4 import io.livekit.android.dagger.InjectionNames 4 import io.livekit.android.dagger.InjectionNames
5 import io.livekit.android.events.TrackEvent 5 import io.livekit.android.events.TrackEvent
  6 +import io.livekit.android.room.track.video.ComposeVisibility
6 import io.livekit.android.room.track.video.VideoSinkVisibility 7 import io.livekit.android.room.track.video.VideoSinkVisibility
7 import io.livekit.android.room.track.video.ViewVisibility 8 import io.livekit.android.room.track.video.ViewVisibility
8 import io.livekit.android.util.LKLog 9 import io.livekit.android.util.LKLog
@@ -28,6 +29,13 @@ class RemoteVideoTrack( @@ -28,6 +29,13 @@ class RemoteVideoTrack(
28 internal var lastDimensions: Dimensions = Dimensions(0, 0) 29 internal var lastDimensions: Dimensions = Dimensions(0, 0)
29 private set 30 private set
30 31
  32 + /**
  33 + * If `autoManageVideo` is enabled, a VideoSinkVisibility should be passed, using
  34 + * [ViewVisibility] if using a traditional View layout, or [ComposeVisibility]
  35 + * if using Jetpack Compose.
  36 + *
  37 + * By default, any Views passed to this method will be added with a [ViewVisibility].
  38 + */
31 override fun addRenderer(renderer: VideoSink) { 39 override fun addRenderer(renderer: VideoSink) {
32 if (autoManageVideo && renderer is View) { 40 if (autoManageVideo && renderer is View) {
33 addRenderer(renderer, ViewVisibility(renderer)) 41 addRenderer(renderer, ViewVisibility(renderer))
@@ -11,8 +11,8 @@ open class FlowCollector<T>( @@ -11,8 +11,8 @@ open class FlowCollector<T>(
11 private val flow: Flow<T>, 11 private val flow: Flow<T>,
12 coroutineScope: CoroutineScope 12 coroutineScope: CoroutineScope
13 ) { 13 ) {
14 - val signal = MutableStateFlow<Unit?>(null)  
15 - val collectEventsDeferred = coroutineScope.async { 14 + private val signal = MutableStateFlow<Unit?>(null)
  15 + private val collectEventsDeferred = coroutineScope.async {
16 flow.toListUntilSignal(signal) 16 flow.toListUntilSignal(signal)
17 } 17 }
18 18
@@ -113,8 +113,8 @@ class ParticipantTest { @@ -113,8 +113,8 @@ class ParticipantTest {
113 113
114 assertEquals(1, participant.tracks.values.size) 114 assertEquals(1, participant.tracks.values.size)
115 assertEquals(audioPublication, participant.tracks.values.first()) 115 assertEquals(audioPublication, participant.tracks.values.first())
116 - assertEquals(1, participant.audioTracks.values.size)  
117 - assertEquals(audioPublication, participant.audioTracks.values.first()) 116 + assertEquals(1, participant.audioTracks.size)
  117 + assertEquals(audioPublication, participant.audioTracks.first().first)
118 } 118 }
119 119
120 companion object { 120 companion object {
1 package io.livekit.android.room.participant 1 package io.livekit.android.room.participant
2 2
3 import io.livekit.android.BaseTest 3 import io.livekit.android.BaseTest
  4 +import io.livekit.android.events.FlowCollector
4 import io.livekit.android.room.SignalClient 5 import io.livekit.android.room.SignalClient
5 import io.livekit.android.room.track.TrackPublication 6 import io.livekit.android.room.track.TrackPublication
6 import io.livekit.android.util.flow 7 import io.livekit.android.util.flow
7 import kotlinx.coroutines.ExperimentalCoroutinesApi 8 import kotlinx.coroutines.ExperimentalCoroutinesApi
8 -import kotlinx.coroutines.launch  
9 import livekit.LivekitModels 9 import livekit.LivekitModels
10 import org.junit.Assert.* 10 import org.junit.Assert.*
11 import org.junit.Before 11 import org.junit.Before
@@ -65,20 +65,12 @@ class RemoteParticipantTest : BaseTest() { @@ -65,20 +65,12 @@ class RemoteParticipantTest : BaseTest() {
65 .addTracks(TRACK_INFO) 65 .addTracks(TRACK_INFO)
66 .build() 66 .build()
67 67
68 - val emissions = mutableListOf<Map<String, TrackPublication>>()  
69 - val job = launch {  
70 - participant::tracks.flow.collect {  
71 - emissions.add(it)  
72 - }  
73 - }  
74 -  
75 - assertEquals(1, emissions.size)  
76 - assertEquals(emptyMap<String, TrackPublication>(), emissions.first())  
77 - 68 + val collector = FlowCollector(participant::tracks.flow, coroutineRule.scope)
78 participant.updateFromInfo(newTrackInfo) 69 participant.updateFromInfo(newTrackInfo)
79 70
80 - job.cancel() 71 + val emissions = collector.stopCollecting()
81 assertEquals(2, emissions.size) 72 assertEquals(2, emissions.size)
  73 + assertEquals(emptyMap<String, TrackPublication>(), emissions[0])
82 assertEquals(1, emissions[1].size) 74 assertEquals(1, emissions[1].size)
83 } 75 }
84 76
@@ -89,20 +81,12 @@ class RemoteParticipantTest : BaseTest() { @@ -89,20 +81,12 @@ class RemoteParticipantTest : BaseTest() {
89 .addTracks(TRACK_INFO) 81 .addTracks(TRACK_INFO)
90 .build() 82 .build()
91 83
92 - val emissions = mutableListOf<Map<String, TrackPublication>>()  
93 - val job = launch {  
94 - participant::audioTracks.flow.collect {  
95 - emissions.add(it)  
96 - }  
97 - }  
98 -  
99 - assertEquals(1, emissions.size)  
100 - assertEquals(emptyMap<String, TrackPublication>(), emissions.first())  
101 - 84 + val collector = FlowCollector(participant::tracks.flow, coroutineRule.scope)
102 participant.updateFromInfo(newTrackInfo) 85 participant.updateFromInfo(newTrackInfo)
103 86
104 - job.cancel() 87 + val emissions = collector.stopCollecting()
105 assertEquals(2, emissions.size) 88 assertEquals(2, emissions.size)
  89 + assertEquals(emptyMap<String, TrackPublication>(), emissions[0])
106 assertEquals(1, emissions[1].size) 90 assertEquals(1, emissions[1].size)
107 } 91 }
108 92
@@ -9,8 +9,6 @@ android { @@ -9,8 +9,6 @@ android {
9 defaultConfig { 9 defaultConfig {
10 minSdk androidSdk.minVersion 10 minSdk androidSdk.minVersion
11 targetSdk androidSdk.targetVersion 11 targetSdk androidSdk.targetVersion
12 - versionCode 1  
13 - versionName "1.0"  
14 consumerProguardFiles 'consumer-rules.pro' 12 consumerProguardFiles 'consumer-rules.pro'
15 13
16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -34,7 +34,6 @@ fun ParticipantItem( @@ -34,7 +34,6 @@ fun ParticipantItem(
34 ) { 34 ) {
35 35
36 val identity by participant::identity.flow.collectAsState() 36 val identity by participant::identity.flow.collectAsState()
37 - val videoTracks by participant::videoTracks.flow.collectAsState()  
38 val audioTracks by participant::audioTracks.flow.collectAsState() 37 val audioTracks by participant::audioTracks.flow.collectAsState()
39 val identityBarPadding = 4.dp 38 val identityBarPadding = 4.dp
40 ConstraintLayout( 39 ConstraintLayout(
@@ -86,7 +85,7 @@ fun ParticipantItem( @@ -86,7 +85,7 @@ fun ParticipantItem(
86 }, 85 },
87 ) 86 )
88 87
89 - val isMuted = audioTracks.values.none { it.track != null && !it.muted } 88 + val isMuted = audioTracks.none { (pub) -> pub.track != null && !pub.muted }
90 89
91 if (isMuted) { 90 if (isMuted) {
92 Icon( 91 Icon(
@@ -6,86 +6,16 @@ import androidx.compose.runtime.* @@ -6,86 +6,16 @@ import androidx.compose.runtime.*
6 import androidx.compose.ui.Alignment 6 import androidx.compose.ui.Alignment
7 import androidx.compose.ui.Modifier 7 import androidx.compose.ui.Modifier
8 import androidx.compose.ui.graphics.Color 8 import androidx.compose.ui.graphics.Color
9 -import androidx.compose.ui.layout.onGloballyPositioned  
10 import androidx.compose.ui.res.painterResource 9 import androidx.compose.ui.res.painterResource
11 -import androidx.compose.ui.viewinterop.AndroidView  
12 -import com.github.ajalt.timberkt.Timber  
13 -import io.livekit.android.renderer.TextureViewRenderer 10 +import io.livekit.android.compose.VideoRenderer
14 import io.livekit.android.room.Room 11 import io.livekit.android.room.Room
15 import io.livekit.android.room.participant.Participant 12 import io.livekit.android.room.participant.Participant
16 -import io.livekit.android.room.track.RemoteVideoTrack  
17 import io.livekit.android.room.track.Track 13 import io.livekit.android.room.track.Track
18 import io.livekit.android.room.track.VideoTrack 14 import io.livekit.android.room.track.VideoTrack
19 -import io.livekit.android.room.track.video.ComposeVisibility  
20 import io.livekit.android.util.flow 15 import io.livekit.android.util.flow
21 import kotlinx.coroutines.flow.* 16 import kotlinx.coroutines.flow.*
22 17
23 /** 18 /**
24 - * Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use  
25 - * [TextureViewRenderer].  
26 - */  
27 -@Composable  
28 -fun VideoItem(  
29 - room: Room,  
30 - videoTrack: VideoTrack,  
31 - modifier: Modifier = Modifier  
32 -) {  
33 -  
34 - val videoSinkVisibility = remember(room, videoTrack) { ComposeVisibility() }  
35 - var boundVideoTrack by remember { mutableStateOf<VideoTrack?>(null) }  
36 - var view: TextureViewRenderer? by remember { mutableStateOf(null) }  
37 -  
38 - fun cleanupVideoTrack() {  
39 - view?.let { boundVideoTrack?.removeRenderer(it) }  
40 - boundVideoTrack = null  
41 - }  
42 -  
43 - fun setupVideoIfNeeded(videoTrack: VideoTrack, view: TextureViewRenderer) {  
44 - if (boundVideoTrack == videoTrack) {  
45 - return  
46 - }  
47 -  
48 - cleanupVideoTrack()  
49 -  
50 - boundVideoTrack = videoTrack  
51 - if (videoTrack is RemoteVideoTrack) {  
52 - videoTrack.addRenderer(view, videoSinkVisibility)  
53 - } else {  
54 - videoTrack.addRenderer(view)  
55 - }  
56 - }  
57 -  
58 - DisposableEffect(room, videoTrack) {  
59 - onDispose {  
60 - videoSinkVisibility.onDispose()  
61 - cleanupVideoTrack()  
62 - }  
63 - }  
64 -  
65 - DisposableEffect(currentCompositeKeyHash.toString()) {  
66 - onDispose {  
67 - view?.release()  
68 - }  
69 - }  
70 -  
71 - AndroidView(  
72 - factory = { context ->  
73 - TextureViewRenderer(context).apply {  
74 - room.initVideoRenderer(this)  
75 - setupVideoIfNeeded(videoTrack, this)  
76 -  
77 - view = this  
78 - }  
79 - },  
80 - update = { view ->  
81 - setupVideoIfNeeded(videoTrack, view)  
82 - },  
83 - modifier = modifier  
84 - .onGloballyPositioned { videoSinkVisibility.onGloballyPositioned(it) },  
85 - )  
86 -}  
87 -  
88 -/**  
89 * This widget primarily serves as a way to observe changes in [videoTracks]. 19 * This widget primarily serves as a way to observe changes in [videoTracks].
90 */ 20 */
91 @Composable 21 @Composable
@@ -94,32 +24,15 @@ fun VideoItemTrackSelector( @@ -94,32 +24,15 @@ fun VideoItemTrackSelector(
94 participant: Participant, 24 participant: Participant,
95 modifier: Modifier = Modifier 25 modifier: Modifier = Modifier
96 ) { 26 ) {
97 -  
98 - val subscribedVideoTracksFlow by remember(participant) {  
99 - mutableStateOf(  
100 - participant::videoTracks.flow  
101 - .flatMapLatest { videoTracks ->  
102 - combine(  
103 - videoTracks.values  
104 - .map { trackPublication ->  
105 - // Re-emit when track changes  
106 - trackPublication::track.flow  
107 - .map { trackPublication }  
108 - }  
109 - ) { trackPubs ->  
110 - trackPubs.toList().filter { trackPublication -> trackPublication.track != null }  
111 - }  
112 - }  
113 - )  
114 - }  
115 -  
116 - val videoTracks by subscribedVideoTracksFlow.collectAsState(initial = emptyList())  
117 - val videoTrack = videoTracks.firstOrNull { pub -> pub.source == Track.Source.SCREEN_SHARE }?.track as? VideoTrack  
118 - ?: videoTracks.firstOrNull { pub -> pub.source == Track.Source.CAMERA }?.track as? VideoTrack  
119 - ?: videoTracks.firstOrNull()?.track as? VideoTrack 27 + val videoTrackMap by participant::videoTracks.flow.collectAsState(initial = emptyList())
  28 + val videoPubs = videoTrackMap.filter { (pub) -> pub.subscribed }
  29 + .map { (pub) -> pub }
  30 + val videoTrack = videoPubs.firstOrNull { pub -> pub.source == Track.Source.SCREEN_SHARE }?.track as? VideoTrack
  31 + ?: videoPubs.firstOrNull { pub -> pub.source == Track.Source.CAMERA }?.track as? VideoTrack
  32 + ?: videoPubs.firstOrNull()?.track as? VideoTrack
120 33
121 if (videoTrack != null) { 34 if (videoTrack != null) {
122 - VideoItem( 35 + VideoRenderer(
123 room = room, 36 room = room,
124 videoTrack = videoTrack, 37 videoTrack = videoTrack,
125 modifier = modifier 38 modifier = modifier
1 package io.livekit.android.sample 1 package io.livekit.android.sample
2 2
3 import android.app.Activity 3 import android.app.Activity
4 -import android.content.DialogInterface  
5 import android.media.AudioManager 4 import android.media.AudioManager
6 import android.media.projection.MediaProjectionManager 5 import android.media.projection.MediaProjectionManager
7 import android.os.Bundle 6 import android.os.Bundle
@@ -97,13 +96,10 @@ class CallActivity : AppCompatActivity() { @@ -97,13 +96,10 @@ class CallActivity : AppCompatActivity() {
97 } 96 }
98 }.flatMapLatest { (participant, videoTracks) -> 97 }.flatMapLatest { (participant, videoTracks) ->
99 98
100 - for (videoTrack in videoTracks.values) {  
101 - Timber.e { "videoTrack is ${videoTrack.track}" }  
102 - }  
103 // Prioritize any screenshare streams. 99 // Prioritize any screenshare streams.
104 val trackPublication = participant.getTrackPublication(Track.Source.SCREEN_SHARE) 100 val trackPublication = participant.getTrackPublication(Track.Source.SCREEN_SHARE)
105 ?: participant.getTrackPublication(Track.Source.CAMERA) 101 ?: participant.getTrackPublication(Track.Source.CAMERA)
106 - ?: videoTracks.values.firstOrNull() 102 + ?: videoTracks.firstOrNull()?.first
107 ?: return@flatMapLatest emptyFlow() 103 ?: return@flatMapLatest emptyFlow()
108 104
109 trackPublication::track.flow 105 trackPublication::track.flow
@@ -49,7 +49,7 @@ class ParticipantItem( @@ -49,7 +49,7 @@ class ParticipantItem(
49 coroutineScope?.launch { 49 coroutineScope?.launch {
50 participant::audioTracks.flow 50 participant::audioTracks.flow
51 .flatMapLatest { tracks -> 51 .flatMapLatest { tracks ->
52 - val audioTrack = tracks.values.firstOrNull() 52 + val audioTrack = tracks.firstOrNull()?.first
53 if (audioTrack != null) { 53 if (audioTrack != null) {
54 audioTrack::muted.flow 54 audioTrack::muted.flow
55 } else { 55 } else {
@@ -86,7 +86,7 @@ fun ParticipantItem( @@ -86,7 +86,7 @@ fun ParticipantItem(
86 }, 86 },
87 ) 87 )
88 88
89 - val isMuted = audioTracks.values.none { it.track != null && !it.muted } 89 + val isMuted = audioTracks.none { (pub) -> pub.track != null && !pub.muted }
90 90
91 if (isMuted) { 91 if (isMuted) {
92 Icon( 92 Icon(
@@ -121,29 +121,12 @@ fun VideoItemTrackSelector( @@ -121,29 +121,12 @@ fun VideoItemTrackSelector(
121 participant: Participant, 121 participant: Participant,
122 modifier: Modifier = Modifier 122 modifier: Modifier = Modifier
123 ) { 123 ) {
124 -  
125 - val subscribedVideoTracksFlow by remember(participant) {  
126 - mutableStateOf(  
127 - participant::videoTracks.flow  
128 - .flatMapLatest { videoTracks ->  
129 - combine(  
130 - videoTracks.values  
131 - .map { trackPublication ->  
132 - // Re-emit when track changes  
133 - trackPublication::track.flow  
134 - .map { trackPublication }  
135 - }  
136 - ) { trackPubs ->  
137 - trackPubs.toList().filter { trackPublication -> trackPublication.track != null }  
138 - }  
139 - }  
140 - )  
141 - }  
142 -  
143 - val videoTracks by subscribedVideoTracksFlow.collectAsState(initial = emptyList())  
144 - val videoTrack = videoTracks.firstOrNull { pub -> pub.source == Track.Source.SCREEN_SHARE }?.track as? VideoTrack  
145 - ?: videoTracks.firstOrNull { pub -> pub.source == Track.Source.CAMERA }?.track as? VideoTrack  
146 - ?: videoTracks.firstOrNull()?.track as? VideoTrack 124 + val videoTrackMap by participant::videoTracks.flow.collectAsState(initial = emptyList())
  125 + val videoPubs = videoTrackMap.filter { (pub) -> pub.subscribed }
  126 + .map { (pub) -> pub }
  127 + val videoTrack = videoPubs.firstOrNull { pub -> pub.source == Track.Source.SCREEN_SHARE }?.track as? VideoTrack
  128 + ?: videoPubs.firstOrNull { pub -> pub.source == Track.Source.CAMERA }?.track as? VideoTrack
  129 + ?: videoPubs.firstOrNull()?.track as? VideoTrack
147 130
148 if (videoTrack != null) { 131 if (videoTrack != null) {
149 VideoItem( 132 VideoItem(