davidliu
Committed by GitHub

fix state not being recomputed when track is attached (#61)

@@ -7,7 +7,7 @@ import kotlinx.coroutines.test.runTest @@ -7,7 +7,7 @@ import kotlinx.coroutines.test.runTest
7 import org.junit.Rule 7 import org.junit.Rule
8 import org.mockito.junit.MockitoJUnit 8 import org.mockito.junit.MockitoJUnit
9 9
10 -@ExperimentalCoroutinesApi 10 +@OptIn(ExperimentalCoroutinesApi::class)
11 abstract class BaseTest { 11 abstract class BaseTest {
12 // Uncomment to enable logging in tests. 12 // Uncomment to enable logging in tests.
13 //@get:Rule 13 //@get:Rule
@@ -19,6 +19,6 @@ abstract class BaseTest { @@ -19,6 +19,6 @@ abstract class BaseTest {
19 @get:Rule 19 @get:Rule
20 var coroutineRule = TestCoroutineRule() 20 var coroutineRule = TestCoroutineRule()
21 21
22 - @ExperimentalCoroutinesApi 22 + @OptIn(ExperimentalCoroutinesApi::class)
23 fun runTest(testBody: suspend TestScope.() -> Unit) = coroutineRule.scope.runTest(testBody = testBody) 23 fun runTest(testBody: suspend TestScope.() -> Unit) = coroutineRule.scope.runTest(testBody = testBody)
24 } 24 }
1 package io.livekit.android.room.participant 1 package io.livekit.android.room.participant
2 2
3 -import io.livekit.android.coroutines.TestCoroutineRule 3 +import io.livekit.android.BaseTest
4 import io.livekit.android.room.SignalClient 4 import io.livekit.android.room.SignalClient
  5 +import io.livekit.android.room.track.TrackPublication
  6 +import io.livekit.android.util.flow
  7 +import kotlinx.coroutines.ExperimentalCoroutinesApi
  8 +import kotlinx.coroutines.launch
5 import livekit.LivekitModels 9 import livekit.LivekitModels
6 import org.junit.Assert.* 10 import org.junit.Assert.*
7 import org.junit.Before 11 import org.junit.Before
8 -import org.junit.Rule  
9 import org.junit.Test 12 import org.junit.Test
10 import org.mockito.Mockito 13 import org.mockito.Mockito
11 14
12 -class RemoteParticipantTest { 15 +@OptIn(ExperimentalCoroutinesApi::class)
  16 +class RemoteParticipantTest : BaseTest() {
13 17
14 - @get:Rule  
15 - var coroutineRule = TestCoroutineRule()  
16 18
17 lateinit var signalClient: SignalClient 19 lateinit var signalClient: SignalClient
18 lateinit var participant: RemoteParticipant 20 lateinit var participant: RemoteParticipant
@@ -50,7 +52,6 @@ class RemoteParticipantTest { @@ -50,7 +52,6 @@ class RemoteParticipantTest {
50 val newTrackInfo = LivekitModels.ParticipantInfo.newBuilder(INFO) 52 val newTrackInfo = LivekitModels.ParticipantInfo.newBuilder(INFO)
51 .addTracks(TRACK_INFO) 53 .addTracks(TRACK_INFO)
52 .build() 54 .build()
53 -  
54 participant.updateFromInfo(newTrackInfo) 55 participant.updateFromInfo(newTrackInfo)
55 56
56 assertEquals(1, participant.tracks.values.size) 57 assertEquals(1, participant.tracks.values.size)
@@ -58,6 +59,54 @@ class RemoteParticipantTest { @@ -58,6 +59,54 @@ class RemoteParticipantTest {
58 } 59 }
59 60
60 @Test 61 @Test
  62 + fun tracksFlow() = runTest {
  63 +
  64 + val newTrackInfo = LivekitModels.ParticipantInfo.newBuilder(INFO)
  65 + .addTracks(TRACK_INFO)
  66 + .build()
  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 +
  78 + participant.updateFromInfo(newTrackInfo)
  79 +
  80 + job.cancel()
  81 + assertEquals(2, emissions.size)
  82 + assertEquals(1, emissions[1].size)
  83 + }
  84 +
  85 + @Test
  86 + fun audioTracksFlow() = runTest {
  87 +
  88 + val newTrackInfo = LivekitModels.ParticipantInfo.newBuilder(INFO)
  89 + .addTracks(TRACK_INFO)
  90 + .build()
  91 +
  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 +
  102 + participant.updateFromInfo(newTrackInfo)
  103 +
  104 + job.cancel()
  105 + assertEquals(2, emissions.size)
  106 + assertEquals(1, emissions[1].size)
  107 + }
  108 +
  109 + @Test
61 fun updateFromInfoRemovesTrack() { 110 fun updateFromInfoRemovesTrack() {
62 val newTrackInfo = LivekitModels.ParticipantInfo.newBuilder(INFO) 111 val newTrackInfo = LivekitModels.ParticipantInfo.newBuilder(INFO)
63 .addTracks(TRACK_INFO) 112 .addTracks(TRACK_INFO)
@@ -2,7 +2,6 @@ package io.livekit.android.composesample @@ -2,7 +2,6 @@ package io.livekit.android.composesample
2 2
3 import androidx.compose.foundation.background 3 import androidx.compose.foundation.background
4 import androidx.compose.foundation.border 4 import androidx.compose.foundation.border
5 -import androidx.compose.foundation.layout.fillMaxSize  
6 import androidx.compose.material.Icon 5 import androidx.compose.material.Icon
7 import androidx.compose.material.Surface 6 import androidx.compose.material.Surface
8 import androidx.compose.material.Text 7 import androidx.compose.material.Text
@@ -21,8 +20,6 @@ import io.livekit.android.composesample.ui.theme.NoVideoBackground @@ -21,8 +20,6 @@ import io.livekit.android.composesample.ui.theme.NoVideoBackground
21 import io.livekit.android.room.Room 20 import io.livekit.android.room.Room
22 import io.livekit.android.room.participant.ConnectionQuality 21 import io.livekit.android.room.participant.ConnectionQuality
23 import io.livekit.android.room.participant.Participant 22 import io.livekit.android.room.participant.Participant
24 -import io.livekit.android.room.track.Track  
25 -import io.livekit.android.room.track.VideoTrack  
26 import io.livekit.android.util.flow 23 import io.livekit.android.util.flow
27 24
28 /** 25 /**
@@ -50,33 +47,20 @@ fun ParticipantItem( @@ -50,33 +47,20 @@ fun ParticipantItem(
50 } 47 }
51 } 48 }
52 ) { 49 ) {
53 - val (videoCamOff, identityBar, identityText, muteIndicator, connectionIndicator) = createRefs()  
54 - val videoTrack = participant.getTrackPublication(Track.Source.SCREEN_SHARE)?.track as? VideoTrack  
55 - ?: participant.getTrackPublication(Track.Source.CAMERA)?.track as? VideoTrack  
56 - ?: videoTracks.values.firstOrNull()?.track as? VideoTrack 50 + val (videoItem, identityBar, identityText, muteIndicator, connectionIndicator) = createRefs()
57 51
58 -  
59 - if (videoTrack != null && videoTrack.enabled) {  
60 - VideoItemTrackSelector(  
61 - room = room,  
62 - participant = participant,  
63 - modifier = Modifier.fillMaxSize()  
64 - )  
65 - } else {  
66 - Icon(  
67 - painter = painterResource(id = R.drawable.outline_videocam_off_24),  
68 - contentDescription = null,  
69 - tint = Color.White,  
70 - modifier = Modifier.constrainAs(videoCamOff) {  
71 - top.linkTo(parent.top)  
72 - bottom.linkTo(parent.bottom)  
73 - start.linkTo(parent.start)  
74 - end.linkTo(parent.end)  
75 - width = Dimension.wrapContent  
76 - height = Dimension.wrapContent  
77 - }  
78 - )  
79 - } 52 + VideoItemTrackSelector(
  53 + room = room,
  54 + participant = participant,
  55 + modifier = Modifier.constrainAs(videoItem) {
  56 + top.linkTo(parent.top)
  57 + bottom.linkTo(parent.bottom)
  58 + start.linkTo(parent.start)
  59 + end.linkTo(parent.end)
  60 + width = Dimension.fillToConstraints
  61 + height = Dimension.fillToConstraints
  62 + }
  63 + )
80 64
81 Surface( 65 Surface(
82 color = Color(0x80000000), 66 color = Color(0x80000000),
1 package io.livekit.android.composesample 1 package io.livekit.android.composesample
2 2
  3 +import androidx.compose.foundation.layout.Box
  4 +import androidx.compose.material.Icon
3 import androidx.compose.runtime.* 5 import androidx.compose.runtime.*
  6 +import androidx.compose.ui.Alignment
4 import androidx.compose.ui.Modifier 7 import androidx.compose.ui.Modifier
  8 +import androidx.compose.ui.graphics.Color
5 import androidx.compose.ui.layout.onGloballyPositioned 9 import androidx.compose.ui.layout.onGloballyPositioned
  10 +import androidx.compose.ui.res.painterResource
6 import androidx.compose.ui.viewinterop.AndroidView 11 import androidx.compose.ui.viewinterop.AndroidView
  12 +import com.github.ajalt.timberkt.Timber
7 import io.livekit.android.renderer.TextureViewRenderer 13 import io.livekit.android.renderer.TextureViewRenderer
8 import io.livekit.android.room.Room 14 import io.livekit.android.room.Room
9 import io.livekit.android.room.participant.Participant 15 import io.livekit.android.room.participant.Participant
@@ -12,7 +18,7 @@ import io.livekit.android.room.track.Track @@ -12,7 +18,7 @@ import io.livekit.android.room.track.Track
12 import io.livekit.android.room.track.VideoTrack 18 import io.livekit.android.room.track.VideoTrack
13 import io.livekit.android.room.track.video.ComposeVisibility 19 import io.livekit.android.room.track.video.ComposeVisibility
14 import io.livekit.android.util.flow 20 import io.livekit.android.util.flow
15 -import kotlinx.coroutines.flow.map 21 +import kotlinx.coroutines.flow.*
16 22
17 /** 23 /**
18 * Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use 24 * Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use
@@ -24,6 +30,7 @@ fun VideoItem( @@ -24,6 +30,7 @@ fun VideoItem(
24 videoTrack: VideoTrack, 30 videoTrack: VideoTrack,
25 modifier: Modifier = Modifier 31 modifier: Modifier = Modifier
26 ) { 32 ) {
  33 +
27 val videoSinkVisibility = remember(room, videoTrack) { ComposeVisibility() } 34 val videoSinkVisibility = remember(room, videoTrack) { ComposeVisibility() }
28 var boundVideoTrack by remember { mutableStateOf<VideoTrack?>(null) } 35 var boundVideoTrack by remember { mutableStateOf<VideoTrack?>(null) }
29 var view: TextureViewRenderer? by remember { mutableStateOf(null) } 36 var view: TextureViewRenderer? by remember { mutableStateOf(null) }
@@ -91,9 +98,21 @@ fun VideoItemTrackSelector( @@ -91,9 +98,21 @@ fun VideoItemTrackSelector(
91 val subscribedVideoTracksFlow by remember(participant) { 98 val subscribedVideoTracksFlow by remember(participant) {
92 mutableStateOf( 99 mutableStateOf(
93 participant::videoTracks.flow 100 participant::videoTracks.flow
94 - .map { tracks -> tracks.values.filter { pub -> pub.subscribed } } 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 + }
95 ) 113 )
96 } 114 }
  115 +
97 val videoTracks by subscribedVideoTracksFlow.collectAsState(initial = emptyList()) 116 val videoTracks by subscribedVideoTracksFlow.collectAsState(initial = emptyList())
98 val videoTrack = videoTracks.firstOrNull { pub -> pub.source == Track.Source.SCREEN_SHARE }?.track as? VideoTrack 117 val videoTrack = videoTracks.firstOrNull { pub -> pub.source == Track.Source.SCREEN_SHARE }?.track as? VideoTrack
99 ?: videoTracks.firstOrNull { pub -> pub.source == Track.Source.CAMERA }?.track as? VideoTrack 118 ?: videoTracks.firstOrNull { pub -> pub.source == Track.Source.CAMERA }?.track as? VideoTrack
@@ -105,5 +124,14 @@ fun VideoItemTrackSelector( @@ -105,5 +124,14 @@ fun VideoItemTrackSelector(
105 videoTrack = videoTrack, 124 videoTrack = videoTrack,
106 modifier = modifier 125 modifier = modifier
107 ) 126 )
  127 + } else {
  128 + Box(modifier = modifier) {
  129 + Icon(
  130 + painter = painterResource(id = R.drawable.outline_videocam_off_24),
  131 + contentDescription = null,
  132 + tint = Color.White,
  133 + modifier = Modifier.align(Alignment.Center)
  134 + )
  135 + }
108 } 136 }
109 } 137 }