Committed by
GitHub
android automatic track management (#23)
* basic event bus implementation * automatic video track management * fix tests * clean up import
正在显示
22 个修改的文件
包含
494 行增加
和
37 行删除
| @@ -2,7 +2,7 @@ | @@ -2,7 +2,7 @@ | ||
| 2 | 2 | ||
| 3 | buildscript { | 3 | buildscript { |
| 4 | ext { | 4 | ext { |
| 5 | - compose_version = '1.0.4' | 5 | + compose_version = '1.0.5' |
| 6 | kotlin_version = '1.5.31' | 6 | kotlin_version = '1.5.31' |
| 7 | java_version = JavaVersion.VERSION_1_8 | 7 | java_version = JavaVersion.VERSION_1_8 |
| 8 | dokka_version = '1.5.0' | 8 | dokka_version = '1.5.0' |
| @@ -104,6 +104,7 @@ dependencies { | @@ -104,6 +104,7 @@ dependencies { | ||
| 104 | implementation "androidx.core:core:${versions.androidx_core}" | 104 | implementation "androidx.core:core:${versions.androidx_core}" |
| 105 | implementation "com.google.protobuf:protobuf-java:${versions.protobuf}" | 105 | implementation "com.google.protobuf:protobuf-java:${versions.protobuf}" |
| 106 | implementation "com.google.protobuf:protobuf-java-util:${versions.protobuf}" | 106 | implementation "com.google.protobuf:protobuf-java-util:${versions.protobuf}" |
| 107 | + implementation "androidx.compose.ui:ui:$compose_version" | ||
| 107 | 108 | ||
| 108 | implementation 'com.google.dagger:dagger:2.38' | 109 | implementation 'com.google.dagger:dagger:2.38' |
| 109 | kapt 'com.google.dagger:dagger-compiler:2.38' | 110 | kapt 'com.google.dagger:dagger-compiler:2.38' |
| @@ -8,7 +8,18 @@ import org.webrtc.PeerConnection | @@ -8,7 +8,18 @@ import org.webrtc.PeerConnection | ||
| 8 | 8 | ||
| 9 | 9 | ||
| 10 | data class ConnectOptions( | 10 | data class ConnectOptions( |
| 11 | + /** Auto subscribe to room tracks upon connect, defaults to true */ | ||
| 11 | val autoSubscribe: Boolean = true, | 12 | val autoSubscribe: Boolean = true, |
| 13 | + /** | ||
| 14 | + * Automatically manage quality of subscribed video tracks, subscribe to the | ||
| 15 | + * an appropriate resolution based on the size of the video elements that tracks | ||
| 16 | + * are attached to. | ||
| 17 | + * | ||
| 18 | + * Also observes the visibility of attached tracks and pauses receiving data | ||
| 19 | + * if they are not visible. | ||
| 20 | + */ | ||
| 21 | + val autoManageVideo: Boolean = false, | ||
| 22 | + | ||
| 12 | val iceServers: List<PeerConnection.IceServer>? = null, | 23 | val iceServers: List<PeerConnection.IceServer>? = null, |
| 13 | val rtcConfig: PeerConnection.RTCConfiguration? = null, | 24 | val rtcConfig: PeerConnection.RTCConfiguration? = null, |
| 14 | /** | 25 | /** |
| @@ -61,6 +61,7 @@ class LiveKit { | @@ -61,6 +61,7 @@ class LiveKit { | ||
| 61 | options?.videoTrackPublishDefaults?.let { | 61 | options?.videoTrackPublishDefaults?.let { |
| 62 | room.localParticipant.videoTrackPublishDefaults = it | 62 | room.localParticipant.videoTrackPublishDefaults = it |
| 63 | } | 63 | } |
| 64 | + room.autoManageVideo = options?.autoManageVideo ?: false | ||
| 64 | 65 | ||
| 65 | if (options?.audio == true) { | 66 | if (options?.audio == true) { |
| 66 | val audioTrack = room.localParticipant.createAudioTrack() | 67 | val audioTrack = room.localParticipant.createAudioTrack() |
| 1 | +package io.livekit.android.events | ||
| 2 | + | ||
| 3 | +import kotlinx.coroutines.flow.MutableSharedFlow | ||
| 4 | +import kotlinx.coroutines.flow.asSharedFlow | ||
| 5 | +import kotlinx.coroutines.flow.collect | ||
| 6 | + | ||
| 7 | +class BroadcastEventBus<T> : EventListenable<T> { | ||
| 8 | + private val mutableEvents = MutableSharedFlow<T>() | ||
| 9 | + override val events = mutableEvents.asSharedFlow() | ||
| 10 | + | ||
| 11 | + suspend fun postEvent(event: T) { | ||
| 12 | + mutableEvents.emit(event) | ||
| 13 | + } | ||
| 14 | + | ||
| 15 | + suspend fun postEvents(eventsToPost: Collection<T>) { | ||
| 16 | + eventsToPost.forEach { event -> | ||
| 17 | + mutableEvents.emit(event) | ||
| 18 | + } | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + fun readOnly(): EventListenable<T> = this | ||
| 22 | +} |
| 1 | +package io.livekit.android.events | ||
| 2 | + | ||
| 3 | +import kotlinx.coroutines.flow.SharedFlow | ||
| 4 | +import kotlinx.coroutines.flow.collect | ||
| 5 | + | ||
| 6 | +interface EventListenable<out T> { | ||
| 7 | + val events: SharedFlow<T> | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +suspend inline fun <T> EventListenable<T>.collect(crossinline action: suspend (value: T) -> Unit) { | ||
| 11 | + return events.collect(action) | ||
| 12 | +} |
| @@ -10,13 +10,16 @@ import dagger.assisted.AssistedFactory | @@ -10,13 +10,16 @@ import dagger.assisted.AssistedFactory | ||
| 10 | import dagger.assisted.AssistedInject | 10 | import dagger.assisted.AssistedInject |
| 11 | import io.livekit.android.ConnectOptions | 11 | import io.livekit.android.ConnectOptions |
| 12 | import io.livekit.android.Version | 12 | import io.livekit.android.Version |
| 13 | +import io.livekit.android.dagger.InjectionNames | ||
| 13 | import io.livekit.android.renderer.TextureViewRenderer | 14 | import io.livekit.android.renderer.TextureViewRenderer |
| 14 | import io.livekit.android.room.participant.* | 15 | import io.livekit.android.room.participant.* |
| 15 | import io.livekit.android.room.track.* | 16 | import io.livekit.android.room.track.* |
| 16 | import io.livekit.android.util.LKLog | 17 | import io.livekit.android.util.LKLog |
| 18 | +import kotlinx.coroutines.CoroutineDispatcher | ||
| 17 | import livekit.LivekitModels | 19 | import livekit.LivekitModels |
| 18 | import livekit.LivekitRtc | 20 | import livekit.LivekitRtc |
| 19 | import org.webrtc.* | 21 | import org.webrtc.* |
| 22 | +import javax.inject.Named | ||
| 20 | 23 | ||
| 21 | class Room | 24 | class Room |
| 22 | @AssistedInject | 25 | @AssistedInject |
| @@ -26,6 +29,8 @@ constructor( | @@ -26,6 +29,8 @@ constructor( | ||
| 26 | private val eglBase: EglBase, | 29 | private val eglBase: EglBase, |
| 27 | private val localParticipantFactory: LocalParticipant.Factory, | 30 | private val localParticipantFactory: LocalParticipant.Factory, |
| 28 | private val defaultsManager: DefaultsManager, | 31 | private val defaultsManager: DefaultsManager, |
| 32 | + @Named(InjectionNames.DISPATCHER_IO) | ||
| 33 | + private val ioDispatcher: CoroutineDispatcher, | ||
| 29 | ) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() { | 34 | ) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() { |
| 30 | init { | 35 | init { |
| 31 | engine.listener = this | 36 | engine.listener = this |
| @@ -52,6 +57,7 @@ constructor( | @@ -52,6 +57,7 @@ constructor( | ||
| 52 | var metadata: String? = null | 57 | var metadata: String? = null |
| 53 | private set | 58 | private set |
| 54 | 59 | ||
| 60 | + var autoManageVideo: Boolean = false | ||
| 55 | var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults | 61 | var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults |
| 56 | var audioTrackPublishDefaults: AudioTrackPublishDefaults by defaultsManager::audioTrackPublishDefaults | 62 | var audioTrackPublishDefaults: AudioTrackPublishDefaults by defaultsManager::audioTrackPublishDefaults |
| 57 | var videoTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::videoTrackCaptureDefaults | 63 | var videoTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::videoTrackCaptureDefaults |
| @@ -121,9 +127,9 @@ constructor( | @@ -121,9 +127,9 @@ constructor( | ||
| 121 | } | 127 | } |
| 122 | 128 | ||
| 123 | participant = if (info != null) { | 129 | participant = if (info != null) { |
| 124 | - RemoteParticipant(engine.client, info) | 130 | + RemoteParticipant(info, engine.client, ioDispatcher) |
| 125 | } else { | 131 | } else { |
| 126 | - RemoteParticipant(engine.client, sid, null) | 132 | + RemoteParticipant(sid, null, engine.client, ioDispatcher) |
| 127 | } | 133 | } |
| 128 | participant.internalListener = this | 134 | participant.internalListener = this |
| 129 | mutableRemoteParticipants[sid] = participant | 135 | mutableRemoteParticipants[sid] = participant |
| @@ -282,7 +288,7 @@ constructor( | @@ -282,7 +288,7 @@ constructor( | ||
| 282 | trackSid = track.id() | 288 | trackSid = track.id() |
| 283 | } | 289 | } |
| 284 | val participant = getOrCreateRemoteParticipant(participantSid) | 290 | val participant = getOrCreateRemoteParticipant(participantSid) |
| 285 | - participant.addSubscribedMediaTrack(track, trackSid!!) | 291 | + participant.addSubscribedMediaTrack(track, trackSid!!, autoManageVideo) |
| 286 | } | 292 | } |
| 287 | 293 | ||
| 288 | /** | 294 | /** |
| @@ -26,6 +26,7 @@ import org.webrtc.PeerConnection | @@ -26,6 +26,7 @@ import org.webrtc.PeerConnection | ||
| 26 | import org.webrtc.SessionDescription | 26 | import org.webrtc.SessionDescription |
| 27 | import javax.inject.Inject | 27 | import javax.inject.Inject |
| 28 | import javax.inject.Named | 28 | import javax.inject.Named |
| 29 | +import javax.inject.Singleton | ||
| 29 | import kotlin.coroutines.Continuation | 30 | import kotlin.coroutines.Continuation |
| 30 | import kotlin.coroutines.suspendCoroutine | 31 | import kotlin.coroutines.suspendCoroutine |
| 31 | 32 | ||
| @@ -33,6 +34,7 @@ import kotlin.coroutines.suspendCoroutine | @@ -33,6 +34,7 @@ import kotlin.coroutines.suspendCoroutine | ||
| 33 | * SignalClient to LiveKit WS servers | 34 | * SignalClient to LiveKit WS servers |
| 34 | * @suppress | 35 | * @suppress |
| 35 | */ | 36 | */ |
| 37 | +@Singleton | ||
| 36 | class SignalClient | 38 | class SignalClient |
| 37 | @Inject | 39 | @Inject |
| 38 | constructor( | 40 | constructor( |
| @@ -288,11 +290,26 @@ constructor( | @@ -288,11 +290,26 @@ constructor( | ||
| 288 | sendRequest(request) | 290 | sendRequest(request) |
| 289 | } | 291 | } |
| 290 | 292 | ||
| 291 | - fun sendUpdateTrackSettings(sid: String, disabled: Boolean, videoQuality: LivekitRtc.VideoQuality) { | 293 | + fun sendUpdateTrackSettings( |
| 294 | + sid: String, | ||
| 295 | + disabled: Boolean, | ||
| 296 | + videoDimensions: Track.Dimensions?, | ||
| 297 | + videoQuality: LivekitRtc.VideoQuality?, | ||
| 298 | + ) { | ||
| 292 | val trackSettings = LivekitRtc.UpdateTrackSettings.newBuilder() | 299 | val trackSettings = LivekitRtc.UpdateTrackSettings.newBuilder() |
| 293 | .addTrackSids(sid) | 300 | .addTrackSids(sid) |
| 294 | .setDisabled(disabled) | 301 | .setDisabled(disabled) |
| 295 | - .setQuality(videoQuality) | 302 | + .apply { |
| 303 | + if(videoDimensions != null) { | ||
| 304 | + width = videoDimensions.width | ||
| 305 | + height = videoDimensions.height | ||
| 306 | + } else if(videoQuality != null) { | ||
| 307 | + quality = videoQuality | ||
| 308 | + } else { | ||
| 309 | + // default to HIGH | ||
| 310 | + quality = LivekitRtc.VideoQuality.HIGH | ||
| 311 | + } | ||
| 312 | + } | ||
| 296 | 313 | ||
| 297 | val request = LivekitRtc.SignalRequest.newBuilder() | 314 | val request = LivekitRtc.SignalRequest.newBuilder() |
| 298 | .setTrackSetting(trackSettings) | 315 | .setTrackSetting(trackSettings) |
| @@ -4,6 +4,7 @@ import io.livekit.android.room.SignalClient | @@ -4,6 +4,7 @@ import io.livekit.android.room.SignalClient | ||
| 4 | import io.livekit.android.room.track.* | 4 | import io.livekit.android.room.track.* |
| 5 | import io.livekit.android.util.CloseableCoroutineScope | 5 | import io.livekit.android.util.CloseableCoroutineScope |
| 6 | import io.livekit.android.util.LKLog | 6 | import io.livekit.android.util.LKLog |
| 7 | +import kotlinx.coroutines.CoroutineDispatcher | ||
| 7 | import kotlinx.coroutines.SupervisorJob | 8 | import kotlinx.coroutines.SupervisorJob |
| 8 | import kotlinx.coroutines.delay | 9 | import kotlinx.coroutines.delay |
| 9 | import kotlinx.coroutines.launch | 10 | import kotlinx.coroutines.launch |
| @@ -13,14 +14,24 @@ import org.webrtc.MediaStreamTrack | @@ -13,14 +14,24 @@ import org.webrtc.MediaStreamTrack | ||
| 13 | import org.webrtc.VideoTrack | 14 | import org.webrtc.VideoTrack |
| 14 | 15 | ||
| 15 | class RemoteParticipant( | 16 | class RemoteParticipant( |
| 16 | - val signalClient: SignalClient, | ||
| 17 | sid: String, | 17 | sid: String, |
| 18 | identity: String? = null, | 18 | identity: String? = null, |
| 19 | + val signalClient: SignalClient, | ||
| 20 | + private val ioDispatcher: CoroutineDispatcher, | ||
| 19 | ) : Participant(sid, identity) { | 21 | ) : Participant(sid, identity) { |
| 20 | /** | 22 | /** |
| 21 | * @suppress | 23 | * @suppress |
| 22 | */ | 24 | */ |
| 23 | - constructor(signalClient: SignalClient, info: LivekitModels.ParticipantInfo) : this(signalClient, info.sid, info.identity) { | 25 | + constructor( |
| 26 | + info: LivekitModels.ParticipantInfo, | ||
| 27 | + signalClient: SignalClient, | ||
| 28 | + ioDispatcher: CoroutineDispatcher | ||
| 29 | + ) : this( | ||
| 30 | + info.sid, | ||
| 31 | + info.identity, | ||
| 32 | + signalClient, | ||
| 33 | + ioDispatcher, | ||
| 34 | + ) { | ||
| 24 | updateFromInfo(info) | 35 | updateFromInfo(info) |
| 25 | } | 36 | } |
| 26 | 37 | ||
| @@ -43,7 +54,11 @@ class RemoteParticipant( | @@ -43,7 +54,11 @@ class RemoteParticipant( | ||
| 43 | var publication = getTrackPublication(trackSid) | 54 | var publication = getTrackPublication(trackSid) |
| 44 | 55 | ||
| 45 | if (publication == null) { | 56 | if (publication == null) { |
| 46 | - publication = RemoteTrackPublication(trackInfo, participant = this) | 57 | + publication = RemoteTrackPublication( |
| 58 | + trackInfo, | ||
| 59 | + participant = this, | ||
| 60 | + ioDispatcher = ioDispatcher | ||
| 61 | + ) | ||
| 47 | 62 | ||
| 48 | newTrackPublications[trackSid] = publication | 63 | newTrackPublications[trackSid] = publication |
| 49 | addTrackPublication(publication) | 64 | addTrackPublication(publication) |
| @@ -71,11 +86,21 @@ class RemoteParticipant( | @@ -71,11 +86,21 @@ class RemoteParticipant( | ||
| 71 | /** | 86 | /** |
| 72 | * @suppress | 87 | * @suppress |
| 73 | */ | 88 | */ |
| 74 | - fun addSubscribedMediaTrack(mediaTrack: MediaStreamTrack, sid: String, triesLeft: Int = 20) { | 89 | + fun addSubscribedMediaTrack( |
| 90 | + mediaTrack: MediaStreamTrack, | ||
| 91 | + sid: String, | ||
| 92 | + autoManageVideo: Boolean = false, | ||
| 93 | + triesLeft: Int = 20 | ||
| 94 | + ) { | ||
| 75 | val publication = getTrackPublication(sid) | 95 | val publication = getTrackPublication(sid) |
| 76 | val track: Track = when (val kind = mediaTrack.kind()) { | 96 | val track: Track = when (val kind = mediaTrack.kind()) { |
| 77 | KIND_AUDIO -> AudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "") | 97 | KIND_AUDIO -> AudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "") |
| 78 | - KIND_VIDEO -> VideoTrack(rtcTrack = mediaTrack as VideoTrack, name = "") | 98 | + KIND_VIDEO -> RemoteVideoTrack( |
| 99 | + rtcTrack = mediaTrack as VideoTrack, | ||
| 100 | + name = "", | ||
| 101 | + autoManageVideo = autoManageVideo, | ||
| 102 | + dispatcher = ioDispatcher | ||
| 103 | + ) | ||
| 79 | else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind") | 104 | else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind") |
| 80 | } | 105 | } |
| 81 | 106 | ||
| @@ -90,7 +115,7 @@ class RemoteParticipant( | @@ -90,7 +115,7 @@ class RemoteParticipant( | ||
| 90 | } else { | 115 | } else { |
| 91 | coroutineScope.launch { | 116 | coroutineScope.launch { |
| 92 | delay(150) | 117 | delay(150) |
| 93 | - addSubscribedMediaTrack(mediaTrack, sid, triesLeft - 1) | 118 | + addSubscribedMediaTrack(mediaTrack, sid, autoManageVideo, triesLeft - 1) |
| 94 | } | 119 | } |
| 95 | } | 120 | } |
| 96 | return | 121 | return |
| 1 | package io.livekit.android.room.track | 1 | package io.livekit.android.room.track |
| 2 | 2 | ||
| 3 | +import io.livekit.android.dagger.InjectionNames | ||
| 4 | +import io.livekit.android.events.TrackEvent | ||
| 5 | +import io.livekit.android.events.collect | ||
| 3 | import io.livekit.android.room.participant.RemoteParticipant | 6 | import io.livekit.android.room.participant.RemoteParticipant |
| 7 | +import io.livekit.android.util.debounce | ||
| 8 | +import io.livekit.android.util.invoke | ||
| 9 | +import kotlinx.coroutines.* | ||
| 4 | import livekit.LivekitModels | 10 | import livekit.LivekitModels |
| 5 | import livekit.LivekitRtc | 11 | import livekit.LivekitRtc |
| 12 | +import javax.inject.Named | ||
| 6 | 13 | ||
| 7 | class RemoteTrackPublication( | 14 | class RemoteTrackPublication( |
| 8 | info: LivekitModels.TrackInfo, | 15 | info: LivekitModels.TrackInfo, |
| 9 | track: Track? = null, | 16 | track: Track? = null, |
| 10 | - participant: RemoteParticipant | 17 | + participant: RemoteParticipant, |
| 18 | + @Named(InjectionNames.DISPATCHER_IO) | ||
| 19 | + private val ioDispatcher: CoroutineDispatcher, | ||
| 11 | ) : TrackPublication(info, track, participant) { | 20 | ) : TrackPublication(info, track, participant) { |
| 12 | 21 | ||
| 22 | + @OptIn(FlowPreview::class) | ||
| 23 | + override var track: Track? | ||
| 24 | + get() = super.track | ||
| 25 | + set(value) { | ||
| 26 | + if (value != super.track) { | ||
| 27 | + trackJob?.cancel() | ||
| 28 | + trackJob = null | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + super.track = value | ||
| 32 | + | ||
| 33 | + if (value != null) { | ||
| 34 | + trackJob = CoroutineScope(ioDispatcher).launch { | ||
| 35 | + value.events.collect { | ||
| 36 | + when (it) { | ||
| 37 | + is TrackEvent.VisibilityChanged -> handleVisibilityChanged(it) | ||
| 38 | + is TrackEvent.VideoDimensionsChanged -> handleVideoDimensionsChanged(it) | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + private fun handleVisibilityChanged(trackEvent: TrackEvent.VisibilityChanged) { | ||
| 46 | + disabled = !trackEvent.isVisible | ||
| 47 | + sendUpdateTrackSettings.invoke() | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + private fun handleVideoDimensionsChanged(trackEvent: TrackEvent.VideoDimensionsChanged) { | ||
| 51 | + videoDimensions = trackEvent.newDimensions | ||
| 52 | + sendUpdateTrackSettings.invoke() | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + private var trackJob: Job? = null | ||
| 56 | + | ||
| 13 | private var unsubscribed: Boolean = false | 57 | private var unsubscribed: Boolean = false |
| 14 | private var disabled: Boolean = false | 58 | private var disabled: Boolean = false |
| 15 | - private var videoQuality: LivekitRtc.VideoQuality = LivekitRtc.VideoQuality.HIGH | 59 | + private var videoQuality: LivekitRtc.VideoQuality? = LivekitRtc.VideoQuality.HIGH |
| 60 | + private var videoDimensions: Track.Dimensions? = null | ||
| 61 | + | ||
| 62 | + val isAutoManaged: Boolean | ||
| 63 | + get() = (track as? RemoteVideoTrack)?.autoManageVideo ?: false | ||
| 16 | 64 | ||
| 17 | override val subscribed: Boolean | 65 | override val subscribed: Boolean |
| 18 | get() { | 66 | get() { |
| @@ -54,24 +102,62 @@ class RemoteTrackPublication( | @@ -54,24 +102,62 @@ class RemoteTrackPublication( | ||
| 54 | * video to reduce bandwidth requirements | 102 | * video to reduce bandwidth requirements |
| 55 | */ | 103 | */ |
| 56 | fun setEnabled(enabled: Boolean) { | 104 | fun setEnabled(enabled: Boolean) { |
| 105 | + if (isAutoManaged || !subscribed || enabled == !disabled) { | ||
| 106 | + return | ||
| 107 | + } | ||
| 57 | disabled = !enabled | 108 | disabled = !enabled |
| 58 | - sendUpdateTrackSettings() | 109 | + sendUpdateTrackSettings.invoke() |
| 59 | } | 110 | } |
| 60 | 111 | ||
| 61 | /** | 112 | /** |
| 62 | - * for tracks that support simulcasting, adjust subscribed quality | 113 | + * for tracks that support simulcasting, directly adjust subscribed quality |
| 63 | * | 114 | * |
| 64 | * this indicates the highest quality the client can accept. if network bandwidth does not | 115 | * this indicates the highest quality the client can accept. if network bandwidth does not |
| 65 | * allow, server will automatically reduce quality to optimize for uninterrupted video | 116 | * allow, server will automatically reduce quality to optimize for uninterrupted video |
| 66 | */ | 117 | */ |
| 67 | fun setVideoQuality(quality: LivekitRtc.VideoQuality) { | 118 | fun setVideoQuality(quality: LivekitRtc.VideoQuality) { |
| 119 | + if (isAutoManaged | ||
| 120 | + || !subscribed | ||
| 121 | + || quality == videoQuality | ||
| 122 | + || track !is VideoTrack | ||
| 123 | + ) { | ||
| 124 | + return | ||
| 125 | + } | ||
| 68 | videoQuality = quality | 126 | videoQuality = quality |
| 69 | - sendUpdateTrackSettings() | 127 | + videoDimensions = null |
| 128 | + sendUpdateTrackSettings.invoke() | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + /** | ||
| 132 | + * Update the dimensions that the server will use for determining the video quality to send down. | ||
| 133 | + */ | ||
| 134 | + fun setVideoDimensions(dimensions: Track.Dimensions) { | ||
| 135 | + if (isAutoManaged | ||
| 136 | + || !subscribed | ||
| 137 | + || videoDimensions == dimensions | ||
| 138 | + || track !is VideoTrack | ||
| 139 | + ) { | ||
| 140 | + return | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + videoQuality = null | ||
| 144 | + videoDimensions = dimensions | ||
| 145 | + sendUpdateTrackSettings.invoke() | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + // Debounce just in case multiple settings get changed at once. | ||
| 149 | + private val sendUpdateTrackSettings = debounce<Unit, Unit>(100L, CoroutineScope(ioDispatcher)) { | ||
| 150 | + sendUpdateTrackSettingsImpl() | ||
| 70 | } | 151 | } |
| 71 | 152 | ||
| 72 | - private fun sendUpdateTrackSettings() { | 153 | + private fun sendUpdateTrackSettingsImpl() { |
| 73 | val participant = this.participant.get() as? RemoteParticipant ?: return | 154 | val participant = this.participant.get() as? RemoteParticipant ?: return |
| 74 | 155 | ||
| 75 | - participant.signalClient.sendUpdateTrackSettings(sid, disabled, videoQuality) | 156 | + participant.signalClient.sendUpdateTrackSettings( |
| 157 | + sid, | ||
| 158 | + disabled, | ||
| 159 | + videoDimensions, | ||
| 160 | + videoQuality | ||
| 161 | + ) | ||
| 76 | } | 162 | } |
| 77 | } | 163 | } |
| 1 | +package io.livekit.android.room.track | ||
| 2 | + | ||
| 3 | +import android.view.View | ||
| 4 | +import io.livekit.android.dagger.InjectionNames | ||
| 5 | +import io.livekit.android.events.TrackEvent | ||
| 6 | +import io.livekit.android.room.track.video.VideoSinkVisibility | ||
| 7 | +import io.livekit.android.room.track.video.ViewVisibility | ||
| 8 | +import io.livekit.android.util.LKLog | ||
| 9 | +import kotlinx.coroutines.CoroutineDispatcher | ||
| 10 | +import kotlinx.coroutines.CoroutineScope | ||
| 11 | +import kotlinx.coroutines.SupervisorJob | ||
| 12 | +import kotlinx.coroutines.launch | ||
| 13 | +import org.webrtc.VideoSink | ||
| 14 | +import javax.inject.Named | ||
| 15 | +import kotlin.math.max | ||
| 16 | + | ||
| 17 | +class RemoteVideoTrack( | ||
| 18 | + name: String, | ||
| 19 | + rtcTrack: org.webrtc.VideoTrack, | ||
| 20 | + val autoManageVideo: Boolean = false, | ||
| 21 | + @Named(InjectionNames.DISPATCHER_DEFAULT) | ||
| 22 | + private val dispatcher: CoroutineDispatcher, | ||
| 23 | +) : VideoTrack(name, rtcTrack) { | ||
| 24 | + | ||
| 25 | + private var coroutineScope = CoroutineScope(dispatcher + SupervisorJob()) | ||
| 26 | + private val sinkVisibilityMap = mutableMapOf<VideoSink, VideoSinkVisibility>() | ||
| 27 | + private val visibilities = sinkVisibilityMap.values | ||
| 28 | + | ||
| 29 | + private var lastVisibility = false | ||
| 30 | + private var lastDimensions: Dimensions = Dimensions(0, 0) | ||
| 31 | + | ||
| 32 | + override fun addRenderer(renderer: VideoSink) { | ||
| 33 | + if (autoManageVideo && renderer is View) { | ||
| 34 | + addRenderer(renderer, ViewVisibility(renderer)) | ||
| 35 | + } else { | ||
| 36 | + super.addRenderer(renderer) | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + fun addRenderer(renderer: VideoSink, visibility: VideoSinkVisibility) { | ||
| 41 | + super.addRenderer(renderer) | ||
| 42 | + if (autoManageVideo) { | ||
| 43 | + sinkVisibilityMap[renderer] = visibility | ||
| 44 | + visibility.addObserver { _, _ -> recalculateVisibility() } | ||
| 45 | + recalculateVisibility() | ||
| 46 | + } else { | ||
| 47 | + LKLog.w { "attempted to tracking video sink visibility on an non auto managed video track." } | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + override fun removeRenderer(renderer: VideoSink) { | ||
| 52 | + super.removeRenderer(renderer) | ||
| 53 | + val visibility = sinkVisibilityMap.remove(renderer) | ||
| 54 | + visibility?.close() | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + override fun stop() { | ||
| 58 | + super.stop() | ||
| 59 | + sinkVisibilityMap.values.forEach { it.close() } | ||
| 60 | + sinkVisibilityMap.clear() | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + private fun hasVisibleSinks(): Boolean { | ||
| 64 | + return visibilities.any { it.isVisible() } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + private fun largestVideoViewSize(): Dimensions { | ||
| 68 | + var maxWidth = 0 | ||
| 69 | + var maxHeight = 0 | ||
| 70 | + visibilities.forEach { visibility -> | ||
| 71 | + val size = visibility.size() | ||
| 72 | + maxWidth = max(maxWidth, size.width) | ||
| 73 | + maxHeight = max(maxHeight, size.height) | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + return Dimensions(maxWidth, maxHeight) | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + private fun recalculateVisibility() { | ||
| 80 | + val isVisible = hasVisibleSinks() | ||
| 81 | + val newDimensions = largestVideoViewSize() | ||
| 82 | + | ||
| 83 | + val eventsToPost = mutableListOf<TrackEvent>() | ||
| 84 | + if (isVisible != lastVisibility) { | ||
| 85 | + lastVisibility = isVisible | ||
| 86 | + eventsToPost.add(TrackEvent.VisibilityChanged(isVisible)) | ||
| 87 | + } | ||
| 88 | + if (newDimensions != lastDimensions) { | ||
| 89 | + lastDimensions = newDimensions | ||
| 90 | + eventsToPost.add(TrackEvent.VideoDimensionsChanged(newDimensions)) | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + if (eventsToPost.any()) { | ||
| 94 | + coroutineScope.launch { | ||
| 95 | + eventBus.postEvents(eventsToPost) | ||
| 96 | + } | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | +} |
| 1 | package io.livekit.android.room.track | 1 | package io.livekit.android.room.track |
| 2 | 2 | ||
| 3 | +import io.livekit.android.events.BroadcastEventBus | ||
| 4 | +import io.livekit.android.events.TrackEvent | ||
| 3 | import livekit.LivekitModels | 5 | import livekit.LivekitModels |
| 4 | import org.webrtc.MediaStreamTrack | 6 | import org.webrtc.MediaStreamTrack |
| 5 | 7 | ||
| @@ -8,6 +10,9 @@ open class Track( | @@ -8,6 +10,9 @@ open class Track( | ||
| 8 | kind: Kind, | 10 | kind: Kind, |
| 9 | open val rtcTrack: MediaStreamTrack | 11 | open val rtcTrack: MediaStreamTrack |
| 10 | ) { | 12 | ) { |
| 13 | + protected val eventBus = BroadcastEventBus<TrackEvent>() | ||
| 14 | + val events = eventBus.readOnly() | ||
| 15 | + | ||
| 11 | var name = name | 16 | var name = name |
| 12 | internal set | 17 | internal set |
| 13 | var kind = kind | 18 | var kind = kind |
| @@ -72,7 +77,7 @@ open class Track( | @@ -72,7 +77,7 @@ open class Track( | ||
| 72 | } | 77 | } |
| 73 | } | 78 | } |
| 74 | 79 | ||
| 75 | - data class Dimensions(var width: Int, var height: Int) | 80 | + data class Dimensions(val width: Int, val height: Int) |
| 76 | 81 | ||
| 77 | open fun start() { | 82 | open fun start() { |
| 78 | rtcTrack.setEnabled(true) | 83 | rtcTrack.setEnabled(true) |
| @@ -83,6 +88,7 @@ open class Track( | @@ -83,6 +88,7 @@ open class Track( | ||
| 83 | } | 88 | } |
| 84 | } | 89 | } |
| 85 | 90 | ||
| 91 | + | ||
| 86 | sealed class TrackException(message: String? = null, cause: Throwable? = null) : | 92 | sealed class TrackException(message: String? = null, cause: Throwable? = null) : |
| 87 | Exception(message, cause) { | 93 | Exception(message, cause) { |
| 88 | class InvalidTrackTypeException(message: String? = null, cause: Throwable? = null) : | 94 | class InvalidTrackTypeException(message: String? = null, cause: Throwable? = null) : |
| @@ -9,7 +9,7 @@ open class TrackPublication( | @@ -9,7 +9,7 @@ open class TrackPublication( | ||
| 9 | track: Track?, | 9 | track: Track?, |
| 10 | participant: Participant | 10 | participant: Participant |
| 11 | ) { | 11 | ) { |
| 12 | - var track: Track? = track | 12 | + open var track: Track? = track |
| 13 | internal set | 13 | internal set |
| 14 | var name: String | 14 | var name: String |
| 15 | internal set | 15 | internal set |
| @@ -5,7 +5,7 @@ import org.webrtc.VideoTrack | @@ -5,7 +5,7 @@ import org.webrtc.VideoTrack | ||
| 5 | 5 | ||
| 6 | open class VideoTrack(name: String, override val rtcTrack: VideoTrack) : | 6 | open class VideoTrack(name: String, override val rtcTrack: VideoTrack) : |
| 7 | Track(name, Kind.VIDEO, rtcTrack) { | 7 | Track(name, Kind.VIDEO, rtcTrack) { |
| 8 | - internal val sinks: MutableList<VideoSink> = ArrayList(); | 8 | + protected val sinks: MutableList<VideoSink> = ArrayList(); |
| 9 | 9 | ||
| 10 | var enabled: Boolean | 10 | var enabled: Boolean |
| 11 | get() = rtcTrack.enabled() | 11 | get() = rtcTrack.enabled() |
| @@ -13,12 +13,12 @@ open class VideoTrack(name: String, override val rtcTrack: VideoTrack) : | @@ -13,12 +13,12 @@ open class VideoTrack(name: String, override val rtcTrack: VideoTrack) : | ||
| 13 | rtcTrack.setEnabled(value) | 13 | rtcTrack.setEnabled(value) |
| 14 | } | 14 | } |
| 15 | 15 | ||
| 16 | - fun addRenderer(renderer: VideoSink) { | 16 | + open fun addRenderer(renderer: VideoSink) { |
| 17 | sinks.add(renderer) | 17 | sinks.add(renderer) |
| 18 | rtcTrack.addSink(renderer) | 18 | rtcTrack.addSink(renderer) |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | - fun removeRenderer(renderer: VideoSink) { | 21 | + open fun removeRenderer(renderer: VideoSink) { |
| 22 | rtcTrack.removeSink(renderer) | 22 | rtcTrack.removeSink(renderer) |
| 23 | sinks.remove(renderer) | 23 | sinks.remove(renderer) |
| 24 | } | 24 | } |
livekit-android-sdk/src/main/java/io/livekit/android/room/track/video/VideoSinkVisibility.kt
0 → 100644
| 1 | +package io.livekit.android.room.track.video | ||
| 2 | + | ||
| 3 | +import android.graphics.Rect | ||
| 4 | +import android.os.Handler | ||
| 5 | +import android.os.Looper | ||
| 6 | +import android.view.View | ||
| 7 | +import android.view.ViewTreeObserver | ||
| 8 | +import androidx.annotation.CallSuper | ||
| 9 | +import androidx.compose.ui.layout.LayoutCoordinates | ||
| 10 | +import io.livekit.android.room.track.Track | ||
| 11 | +import java.util.* | ||
| 12 | + | ||
| 13 | +abstract class VideoSinkVisibility : Observable() { | ||
| 14 | + abstract fun isVisible(): Boolean | ||
| 15 | + abstract fun size(): Track.Dimensions | ||
| 16 | + | ||
| 17 | + /** | ||
| 18 | + * This should be called whenever the visibility or size has changed. | ||
| 19 | + */ | ||
| 20 | + fun notifyChanged() { | ||
| 21 | + setChanged() | ||
| 22 | + notifyObservers() | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * Called when this object is no longer needed and should clean up any unused resources. | ||
| 27 | + */ | ||
| 28 | + @CallSuper | ||
| 29 | + open fun close() { | ||
| 30 | + deleteObservers() | ||
| 31 | + } | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +class ComposeVisibility : VideoSinkVisibility() { | ||
| 35 | + private var lastCoordinates: LayoutCoordinates? = null | ||
| 36 | + | ||
| 37 | + override fun isVisible(): Boolean { | ||
| 38 | + return (lastCoordinates?.isAttached == true && lastCoordinates?.size?.width != 0 && lastCoordinates?.size?.height != 0) | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + override fun size(): Track.Dimensions { | ||
| 42 | + val width = lastCoordinates?.size?.width ?: 0 | ||
| 43 | + val height = lastCoordinates?.size?.height ?: 0 | ||
| 44 | + return Track.Dimensions(width, height) | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + fun onGloballyPositioned(layoutCoordinates: LayoutCoordinates) { | ||
| 48 | + val lastVisible = isVisible() | ||
| 49 | + val lastSize = size() | ||
| 50 | + lastCoordinates = layoutCoordinates | ||
| 51 | + | ||
| 52 | + if (lastVisible != isVisible() || lastSize != size()) { | ||
| 53 | + notifyChanged() | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + fun onDispose() { | ||
| 58 | + if (lastCoordinates == null) { | ||
| 59 | + return | ||
| 60 | + } | ||
| 61 | + lastCoordinates = null | ||
| 62 | + notifyChanged() | ||
| 63 | + } | ||
| 64 | +} | ||
| 65 | + | ||
| 66 | +class ViewVisibility(private val view: View) : VideoSinkVisibility() { | ||
| 67 | + | ||
| 68 | + private val handler = Handler(Looper.getMainLooper()) | ||
| 69 | + private val globalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener { | ||
| 70 | + val lastVisibility = false | ||
| 71 | + val lastSize = Track.Dimensions(0, 0) | ||
| 72 | + | ||
| 73 | + override fun onGlobalLayout() { | ||
| 74 | + handler.removeCallbacksAndMessages(null) | ||
| 75 | + handler.postDelayed({ | ||
| 76 | + var shouldNotify = false | ||
| 77 | + val newVisibility = isVisible() | ||
| 78 | + val newSize = size() | ||
| 79 | + if (newVisibility != lastVisibility) { | ||
| 80 | + shouldNotify = true | ||
| 81 | + } | ||
| 82 | + if (newSize != lastSize) { | ||
| 83 | + shouldNotify = true | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + if (shouldNotify) { | ||
| 87 | + notifyChanged() | ||
| 88 | + } | ||
| 89 | + }, 2000) | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + init { | ||
| 94 | + view.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener) | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + private val loc = IntArray(2) | ||
| 98 | + private val viewRect = Rect() | ||
| 99 | + private val windowRect = Rect() | ||
| 100 | + | ||
| 101 | + private fun isViewAncestorsVisible(view: View): Boolean { | ||
| 102 | + if (view.visibility != View.VISIBLE) { | ||
| 103 | + return false | ||
| 104 | + } | ||
| 105 | + val parent = view.parent as? View | ||
| 106 | + if (parent != null) { | ||
| 107 | + return isViewAncestorsVisible(parent) | ||
| 108 | + } | ||
| 109 | + return true | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + override fun isVisible(): Boolean { | ||
| 113 | + if (view.windowVisibility != View.VISIBLE || !isViewAncestorsVisible(view)) { | ||
| 114 | + return false | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + view.getLocationInWindow(loc) | ||
| 118 | + viewRect.set(loc[0], loc[1], loc[0] + view.width, loc[1] + view.height) | ||
| 119 | + | ||
| 120 | + view.getWindowVisibleDisplayFrame(windowRect) | ||
| 121 | + // Ensure window rect origin is at 0,0 | ||
| 122 | + windowRect.offset(-windowRect.left, -windowRect.top) | ||
| 123 | + | ||
| 124 | + return viewRect.intersect(windowRect) | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + override fun size(): Track.Dimensions { | ||
| 128 | + return Track.Dimensions(view.width, view.height) | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + override fun close() { | ||
| 132 | + super.close() | ||
| 133 | + handler.removeCallbacksAndMessages(null) | ||
| 134 | + view.viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener) | ||
| 135 | + } | ||
| 136 | +} |
| 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.room.SignalClient | 4 | import io.livekit.android.room.SignalClient |
| 4 | import livekit.LivekitModels | 5 | import livekit.LivekitModels |
| 5 | import org.junit.Assert.* | 6 | import org.junit.Assert.* |
| 6 | import org.junit.Before | 7 | import org.junit.Before |
| 8 | +import org.junit.Rule | ||
| 7 | import org.junit.Test | 9 | import org.junit.Test |
| 8 | import org.mockito.Mockito | 10 | import org.mockito.Mockito |
| 9 | 11 | ||
| 10 | class RemoteParticipantTest { | 12 | class RemoteParticipantTest { |
| 11 | 13 | ||
| 14 | + @get:Rule | ||
| 15 | + var coroutineRule = TestCoroutineRule() | ||
| 16 | + | ||
| 12 | lateinit var signalClient: SignalClient | 17 | lateinit var signalClient: SignalClient |
| 13 | lateinit var participant: RemoteParticipant | 18 | lateinit var participant: RemoteParticipant |
| 14 | 19 | ||
| 15 | @Before | 20 | @Before |
| 16 | fun setup() { | 21 | fun setup() { |
| 17 | signalClient = Mockito.mock(SignalClient::class.java) | 22 | signalClient = Mockito.mock(SignalClient::class.java) |
| 18 | - participant = RemoteParticipant(signalClient, "sid") | 23 | + participant = RemoteParticipant( |
| 24 | + "sid", | ||
| 25 | + signalClient = signalClient, | ||
| 26 | + ioDispatcher = coroutineRule.dispatcher | ||
| 27 | + ) | ||
| 19 | } | 28 | } |
| 20 | 29 | ||
| 21 | @Test | 30 | @Test |
| @@ -24,7 +33,7 @@ class RemoteParticipantTest { | @@ -24,7 +33,7 @@ class RemoteParticipantTest { | ||
| 24 | .addTracks(TRACK_INFO) | 33 | .addTracks(TRACK_INFO) |
| 25 | .build() | 34 | .build() |
| 26 | 35 | ||
| 27 | - participant = RemoteParticipant(signalClient, info) | 36 | + participant = RemoteParticipant(info, signalClient, ioDispatcher = coroutineRule.dispatcher) |
| 28 | 37 | ||
| 29 | assertEquals(1, participant.tracks.values.size) | 38 | assertEquals(1, participant.tracks.values.size) |
| 30 | assertNotNull(participant.getTrackPublication(TRACK_INFO.sid)) | 39 | assertNotNull(participant.getTrackPublication(TRACK_INFO.sid)) |
| @@ -3,6 +3,7 @@ package io.livekit.android.composesample | @@ -3,6 +3,7 @@ package io.livekit.android.composesample | ||
| 3 | import androidx.compose.foundation.layout.fillMaxSize | 3 | import androidx.compose.foundation.layout.fillMaxSize |
| 4 | import androidx.compose.runtime.* | 4 | import androidx.compose.runtime.* |
| 5 | import androidx.compose.ui.Modifier | 5 | import androidx.compose.ui.Modifier |
| 6 | +import androidx.compose.ui.layout.onGloballyPositioned | ||
| 6 | import androidx.compose.ui.viewinterop.AndroidView | 7 | import androidx.compose.ui.viewinterop.AndroidView |
| 7 | import com.github.ajalt.timberkt.Timber | 8 | import com.github.ajalt.timberkt.Timber |
| 8 | import io.livekit.android.renderer.TextureViewRenderer | 9 | import io.livekit.android.renderer.TextureViewRenderer |
| @@ -10,39 +11,46 @@ import io.livekit.android.room.Room | @@ -10,39 +11,46 @@ import io.livekit.android.room.Room | ||
| 10 | import io.livekit.android.room.participant.ParticipantListener | 11 | import io.livekit.android.room.participant.ParticipantListener |
| 11 | import io.livekit.android.room.participant.RemoteParticipant | 12 | import io.livekit.android.room.participant.RemoteParticipant |
| 12 | import io.livekit.android.room.track.RemoteTrackPublication | 13 | import io.livekit.android.room.track.RemoteTrackPublication |
| 14 | +import io.livekit.android.room.track.RemoteVideoTrack | ||
| 13 | import io.livekit.android.room.track.Track | 15 | import io.livekit.android.room.track.Track |
| 14 | -import io.livekit.android.room.track.VideoTrack | 16 | +import io.livekit.android.room.track.video.ComposeVisibility |
| 15 | 17 | ||
| 16 | @Composable | 18 | @Composable |
| 17 | fun ParticipantItem( | 19 | fun ParticipantItem( |
| 18 | room: Room, | 20 | room: Room, |
| 19 | participant: RemoteParticipant, | 21 | participant: RemoteParticipant, |
| 20 | ) { | 22 | ) { |
| 23 | + val videoSinkVisibility = remember(room, participant) { ComposeVisibility() } | ||
| 21 | var videoBound by remember(room, participant) { mutableStateOf(false) } | 24 | var videoBound by remember(room, participant) { mutableStateOf(false) } |
| 22 | - fun getVideoTrack(): VideoTrack? { | 25 | + fun getVideoTrack(): RemoteVideoTrack? { |
| 23 | return participant | 26 | return participant |
| 24 | .videoTracks.values | 27 | .videoTracks.values |
| 25 | - .firstOrNull()?.track as? VideoTrack | 28 | + .firstOrNull()?.track as? RemoteVideoTrack |
| 26 | } | 29 | } |
| 27 | 30 | ||
| 28 | - fun setupVideoIfNeeded(videoTrack: VideoTrack, view: TextureViewRenderer) { | 31 | + fun setupVideoIfNeeded(videoTrack: RemoteVideoTrack, view: TextureViewRenderer) { |
| 29 | if (videoBound) { | 32 | if (videoBound) { |
| 30 | return | 33 | return |
| 31 | } | 34 | } |
| 32 | 35 | ||
| 33 | videoBound = true | 36 | videoBound = true |
| 34 | Timber.v { "adding renderer to $videoTrack" } | 37 | Timber.v { "adding renderer to $videoTrack" } |
| 35 | - videoTrack.addRenderer(view) | 38 | + videoTrack.addRenderer(view, videoSinkVisibility) |
| 39 | + } | ||
| 40 | + DisposableEffect(room, participant) { | ||
| 41 | + onDispose { | ||
| 42 | + videoSinkVisibility.onDispose() | ||
| 43 | + } | ||
| 36 | } | 44 | } |
| 37 | - | ||
| 38 | AndroidView( | 45 | AndroidView( |
| 39 | factory = { context -> | 46 | factory = { context -> |
| 40 | TextureViewRenderer(context).apply { | 47 | TextureViewRenderer(context).apply { |
| 41 | room.initVideoRenderer(this) | 48 | room.initVideoRenderer(this) |
| 42 | - | ||
| 43 | } | 49 | } |
| 44 | }, | 50 | }, |
| 45 | - modifier = Modifier.fillMaxSize(), | 51 | + modifier = Modifier |
| 52 | + .fillMaxSize() | ||
| 53 | + .onGloballyPositioned { videoSinkVisibility.onGloballyPositioned(it) }, | ||
| 46 | update = { view -> | 54 | update = { view -> |
| 47 | participant.listener = object : ParticipantListener { | 55 | participant.listener = object : ParticipantListener { |
| 48 | override fun onTrackSubscribed( | 56 | override fun onTrackSubscribed( |
| @@ -50,7 +58,7 @@ fun ParticipantItem( | @@ -50,7 +58,7 @@ fun ParticipantItem( | ||
| 50 | publication: RemoteTrackPublication, | 58 | publication: RemoteTrackPublication, |
| 51 | participant: RemoteParticipant | 59 | participant: RemoteParticipant |
| 52 | ) { | 60 | ) { |
| 53 | - if (track is VideoTrack) { | 61 | + if (track is RemoteVideoTrack) { |
| 54 | setupVideoIfNeeded(track, view) | 62 | setupVideoIfNeeded(track, view) |
| 55 | } | 63 | } |
| 56 | } | 64 | } |
-
请 注册 或 登录 后发表评论