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
正在显示
16 个修改的文件
包含
142 行增加
和
170 行删除
| @@ -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( |
-
请 注册 或 登录 后发表评论