Committed by
GitHub
fix state not being recomputed when track is attached (#61)
正在显示
4 个修改的文件
包含
100 行增加
和
39 行删除
| @@ -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 | } |
-
请 注册 或 登录 后发表评论