davidliu
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 @@
buildscript {
ext {
compose_version = '1.0.4'
compose_version = '1.0.5'
kotlin_version = '1.5.31'
java_version = JavaVersion.VERSION_1_8
dokka_version = '1.5.0'
... ...
... ... @@ -104,6 +104,7 @@ dependencies {
implementation "androidx.core:core:${versions.androidx_core}"
implementation "com.google.protobuf:protobuf-java:${versions.protobuf}"
implementation "com.google.protobuf:protobuf-java-util:${versions.protobuf}"
implementation "androidx.compose.ui:ui:$compose_version"
implementation 'com.google.dagger:dagger:2.38'
kapt 'com.google.dagger:dagger-compiler:2.38'
... ...
... ... @@ -8,7 +8,18 @@ import org.webrtc.PeerConnection
data class ConnectOptions(
/** Auto subscribe to room tracks upon connect, defaults to true */
val autoSubscribe: Boolean = true,
/**
* Automatically manage quality of subscribed video tracks, subscribe to the
* an appropriate resolution based on the size of the video elements that tracks
* are attached to.
*
* Also observes the visibility of attached tracks and pauses receiving data
* if they are not visible.
*/
val autoManageVideo: Boolean = false,
val iceServers: List<PeerConnection.IceServer>? = null,
val rtcConfig: PeerConnection.RTCConfiguration? = null,
/**
... ...
... ... @@ -61,6 +61,7 @@ class LiveKit {
options?.videoTrackPublishDefaults?.let {
room.localParticipant.videoTrackPublishDefaults = it
}
room.autoManageVideo = options?.autoManageVideo ?: false
if (options?.audio == true) {
val audioTrack = room.localParticipant.createAudioTrack()
... ...
package io.livekit.android.events
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.collect
class BroadcastEventBus<T> : EventListenable<T> {
private val mutableEvents = MutableSharedFlow<T>()
override val events = mutableEvents.asSharedFlow()
suspend fun postEvent(event: T) {
mutableEvents.emit(event)
}
suspend fun postEvents(eventsToPost: Collection<T>) {
eventsToPost.forEach { event ->
mutableEvents.emit(event)
}
}
fun readOnly(): EventListenable<T> = this
}
\ No newline at end of file
... ...
package io.livekit.android.events
sealed class Event
\ No newline at end of file
... ...
package io.livekit.android.events
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.collect
interface EventListenable<out T> {
val events: SharedFlow<T>
}
suspend inline fun <T> EventListenable<T>.collect(crossinline action: suspend (value: T) -> Unit) {
return events.collect(action)
}
\ No newline at end of file
... ...
package io.livekit.android.events
import io.livekit.android.room.track.Track
sealed class TrackEvent : Event() {
class VisibilityChanged(val isVisible: Boolean) : TrackEvent()
class VideoDimensionsChanged(val newDimensions: Track.Dimensions) : TrackEvent()
}
\ No newline at end of file
... ...
... ... @@ -10,13 +10,16 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.ConnectOptions
import io.livekit.android.Version
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.participant.*
import io.livekit.android.room.track.*
import io.livekit.android.util.LKLog
import kotlinx.coroutines.CoroutineDispatcher
import livekit.LivekitModels
import livekit.LivekitRtc
import org.webrtc.*
import javax.inject.Named
class Room
@AssistedInject
... ... @@ -26,6 +29,8 @@ constructor(
private val eglBase: EglBase,
private val localParticipantFactory: LocalParticipant.Factory,
private val defaultsManager: DefaultsManager,
@Named(InjectionNames.DISPATCHER_IO)
private val ioDispatcher: CoroutineDispatcher,
) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() {
init {
engine.listener = this
... ... @@ -52,6 +57,7 @@ constructor(
var metadata: String? = null
private set
var autoManageVideo: Boolean = false
var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults
var audioTrackPublishDefaults: AudioTrackPublishDefaults by defaultsManager::audioTrackPublishDefaults
var videoTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::videoTrackCaptureDefaults
... ... @@ -121,9 +127,9 @@ constructor(
}
participant = if (info != null) {
RemoteParticipant(engine.client, info)
RemoteParticipant(info, engine.client, ioDispatcher)
} else {
RemoteParticipant(engine.client, sid, null)
RemoteParticipant(sid, null, engine.client, ioDispatcher)
}
participant.internalListener = this
mutableRemoteParticipants[sid] = participant
... ... @@ -282,7 +288,7 @@ constructor(
trackSid = track.id()
}
val participant = getOrCreateRemoteParticipant(participantSid)
participant.addSubscribedMediaTrack(track, trackSid!!)
participant.addSubscribedMediaTrack(track, trackSid!!, autoManageVideo)
}
/**
... ...
... ... @@ -26,6 +26,7 @@ import org.webrtc.PeerConnection
import org.webrtc.SessionDescription
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine
... ... @@ -33,6 +34,7 @@ import kotlin.coroutines.suspendCoroutine
* SignalClient to LiveKit WS servers
* @suppress
*/
@Singleton
class SignalClient
@Inject
constructor(
... ... @@ -288,11 +290,26 @@ constructor(
sendRequest(request)
}
fun sendUpdateTrackSettings(sid: String, disabled: Boolean, videoQuality: LivekitRtc.VideoQuality) {
fun sendUpdateTrackSettings(
sid: String,
disabled: Boolean,
videoDimensions: Track.Dimensions?,
videoQuality: LivekitRtc.VideoQuality?,
) {
val trackSettings = LivekitRtc.UpdateTrackSettings.newBuilder()
.addTrackSids(sid)
.setDisabled(disabled)
.setQuality(videoQuality)
.apply {
if(videoDimensions != null) {
width = videoDimensions.width
height = videoDimensions.height
} else if(videoQuality != null) {
quality = videoQuality
} else {
// default to HIGH
quality = LivekitRtc.VideoQuality.HIGH
}
}
val request = LivekitRtc.SignalRequest.newBuilder()
.setTrackSetting(trackSettings)
... ...
... ... @@ -4,6 +4,7 @@ import io.livekit.android.room.SignalClient
import io.livekit.android.room.track.*
import io.livekit.android.util.CloseableCoroutineScope
import io.livekit.android.util.LKLog
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
... ... @@ -13,14 +14,24 @@ import org.webrtc.MediaStreamTrack
import org.webrtc.VideoTrack
class RemoteParticipant(
val signalClient: SignalClient,
sid: String,
identity: String? = null,
val signalClient: SignalClient,
private val ioDispatcher: CoroutineDispatcher,
) : Participant(sid, identity) {
/**
* @suppress
*/
constructor(signalClient: SignalClient, info: LivekitModels.ParticipantInfo) : this(signalClient, info.sid, info.identity) {
constructor(
info: LivekitModels.ParticipantInfo,
signalClient: SignalClient,
ioDispatcher: CoroutineDispatcher
) : this(
info.sid,
info.identity,
signalClient,
ioDispatcher,
) {
updateFromInfo(info)
}
... ... @@ -43,7 +54,11 @@ class RemoteParticipant(
var publication = getTrackPublication(trackSid)
if (publication == null) {
publication = RemoteTrackPublication(trackInfo, participant = this)
publication = RemoteTrackPublication(
trackInfo,
participant = this,
ioDispatcher = ioDispatcher
)
newTrackPublications[trackSid] = publication
addTrackPublication(publication)
... ... @@ -71,11 +86,21 @@ class RemoteParticipant(
/**
* @suppress
*/
fun addSubscribedMediaTrack(mediaTrack: MediaStreamTrack, sid: String, triesLeft: Int = 20) {
fun addSubscribedMediaTrack(
mediaTrack: MediaStreamTrack,
sid: String,
autoManageVideo: Boolean = false,
triesLeft: Int = 20
) {
val publication = getTrackPublication(sid)
val track: Track = when (val kind = mediaTrack.kind()) {
KIND_AUDIO -> AudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "")
KIND_VIDEO -> VideoTrack(rtcTrack = mediaTrack as VideoTrack, name = "")
KIND_VIDEO -> RemoteVideoTrack(
rtcTrack = mediaTrack as VideoTrack,
name = "",
autoManageVideo = autoManageVideo,
dispatcher = ioDispatcher
)
else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind")
}
... ... @@ -90,7 +115,7 @@ class RemoteParticipant(
} else {
coroutineScope.launch {
delay(150)
addSubscribedMediaTrack(mediaTrack, sid, triesLeft - 1)
addSubscribedMediaTrack(mediaTrack, sid, autoManageVideo, triesLeft - 1)
}
}
return
... ...
package io.livekit.android.room.track
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.events.TrackEvent
import io.livekit.android.events.collect
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.util.debounce
import io.livekit.android.util.invoke
import kotlinx.coroutines.*
import livekit.LivekitModels
import livekit.LivekitRtc
import javax.inject.Named
class RemoteTrackPublication(
info: LivekitModels.TrackInfo,
track: Track? = null,
participant: RemoteParticipant
participant: RemoteParticipant,
@Named(InjectionNames.DISPATCHER_IO)
private val ioDispatcher: CoroutineDispatcher,
) : TrackPublication(info, track, participant) {
@OptIn(FlowPreview::class)
override var track: Track?
get() = super.track
set(value) {
if (value != super.track) {
trackJob?.cancel()
trackJob = null
}
super.track = value
if (value != null) {
trackJob = CoroutineScope(ioDispatcher).launch {
value.events.collect {
when (it) {
is TrackEvent.VisibilityChanged -> handleVisibilityChanged(it)
is TrackEvent.VideoDimensionsChanged -> handleVideoDimensionsChanged(it)
}
}
}
}
}
private fun handleVisibilityChanged(trackEvent: TrackEvent.VisibilityChanged) {
disabled = !trackEvent.isVisible
sendUpdateTrackSettings.invoke()
}
private fun handleVideoDimensionsChanged(trackEvent: TrackEvent.VideoDimensionsChanged) {
videoDimensions = trackEvent.newDimensions
sendUpdateTrackSettings.invoke()
}
private var trackJob: Job? = null
private var unsubscribed: Boolean = false
private var disabled: Boolean = false
private var videoQuality: LivekitRtc.VideoQuality = LivekitRtc.VideoQuality.HIGH
private var videoQuality: LivekitRtc.VideoQuality? = LivekitRtc.VideoQuality.HIGH
private var videoDimensions: Track.Dimensions? = null
val isAutoManaged: Boolean
get() = (track as? RemoteVideoTrack)?.autoManageVideo ?: false
override val subscribed: Boolean
get() {
... ... @@ -54,24 +102,62 @@ class RemoteTrackPublication(
* video to reduce bandwidth requirements
*/
fun setEnabled(enabled: Boolean) {
if (isAutoManaged || !subscribed || enabled == !disabled) {
return
}
disabled = !enabled
sendUpdateTrackSettings()
sendUpdateTrackSettings.invoke()
}
/**
* for tracks that support simulcasting, adjust subscribed quality
* for tracks that support simulcasting, directly adjust subscribed quality
*
* this indicates the highest quality the client can accept. if network bandwidth does not
* allow, server will automatically reduce quality to optimize for uninterrupted video
*/
fun setVideoQuality(quality: LivekitRtc.VideoQuality) {
if (isAutoManaged
|| !subscribed
|| quality == videoQuality
|| track !is VideoTrack
) {
return
}
videoQuality = quality
sendUpdateTrackSettings()
videoDimensions = null
sendUpdateTrackSettings.invoke()
}
/**
* Update the dimensions that the server will use for determining the video quality to send down.
*/
fun setVideoDimensions(dimensions: Track.Dimensions) {
if (isAutoManaged
|| !subscribed
|| videoDimensions == dimensions
|| track !is VideoTrack
) {
return
}
videoQuality = null
videoDimensions = dimensions
sendUpdateTrackSettings.invoke()
}
// Debounce just in case multiple settings get changed at once.
private val sendUpdateTrackSettings = debounce<Unit, Unit>(100L, CoroutineScope(ioDispatcher)) {
sendUpdateTrackSettingsImpl()
}
private fun sendUpdateTrackSettings() {
private fun sendUpdateTrackSettingsImpl() {
val participant = this.participant.get() as? RemoteParticipant ?: return
participant.signalClient.sendUpdateTrackSettings(sid, disabled, videoQuality)
participant.signalClient.sendUpdateTrackSettings(
sid,
disabled,
videoDimensions,
videoQuality
)
}
}
\ No newline at end of file
... ...
package io.livekit.android.room.track
import android.view.View
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.events.TrackEvent
import io.livekit.android.room.track.video.VideoSinkVisibility
import io.livekit.android.room.track.video.ViewVisibility
import io.livekit.android.util.LKLog
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.webrtc.VideoSink
import javax.inject.Named
import kotlin.math.max
class RemoteVideoTrack(
name: String,
rtcTrack: org.webrtc.VideoTrack,
val autoManageVideo: Boolean = false,
@Named(InjectionNames.DISPATCHER_DEFAULT)
private val dispatcher: CoroutineDispatcher,
) : VideoTrack(name, rtcTrack) {
private var coroutineScope = CoroutineScope(dispatcher + SupervisorJob())
private val sinkVisibilityMap = mutableMapOf<VideoSink, VideoSinkVisibility>()
private val visibilities = sinkVisibilityMap.values
private var lastVisibility = false
private var lastDimensions: Dimensions = Dimensions(0, 0)
override fun addRenderer(renderer: VideoSink) {
if (autoManageVideo && renderer is View) {
addRenderer(renderer, ViewVisibility(renderer))
} else {
super.addRenderer(renderer)
}
}
fun addRenderer(renderer: VideoSink, visibility: VideoSinkVisibility) {
super.addRenderer(renderer)
if (autoManageVideo) {
sinkVisibilityMap[renderer] = visibility
visibility.addObserver { _, _ -> recalculateVisibility() }
recalculateVisibility()
} else {
LKLog.w { "attempted to tracking video sink visibility on an non auto managed video track." }
}
}
override fun removeRenderer(renderer: VideoSink) {
super.removeRenderer(renderer)
val visibility = sinkVisibilityMap.remove(renderer)
visibility?.close()
}
override fun stop() {
super.stop()
sinkVisibilityMap.values.forEach { it.close() }
sinkVisibilityMap.clear()
}
private fun hasVisibleSinks(): Boolean {
return visibilities.any { it.isVisible() }
}
private fun largestVideoViewSize(): Dimensions {
var maxWidth = 0
var maxHeight = 0
visibilities.forEach { visibility ->
val size = visibility.size()
maxWidth = max(maxWidth, size.width)
maxHeight = max(maxHeight, size.height)
}
return Dimensions(maxWidth, maxHeight)
}
private fun recalculateVisibility() {
val isVisible = hasVisibleSinks()
val newDimensions = largestVideoViewSize()
val eventsToPost = mutableListOf<TrackEvent>()
if (isVisible != lastVisibility) {
lastVisibility = isVisible
eventsToPost.add(TrackEvent.VisibilityChanged(isVisible))
}
if (newDimensions != lastDimensions) {
lastDimensions = newDimensions
eventsToPost.add(TrackEvent.VideoDimensionsChanged(newDimensions))
}
if (eventsToPost.any()) {
coroutineScope.launch {
eventBus.postEvents(eventsToPost)
}
}
}
}
\ No newline at end of file
... ...
package io.livekit.android.room.track
import io.livekit.android.events.BroadcastEventBus
import io.livekit.android.events.TrackEvent
import livekit.LivekitModels
import org.webrtc.MediaStreamTrack
... ... @@ -8,6 +10,9 @@ open class Track(
kind: Kind,
open val rtcTrack: MediaStreamTrack
) {
protected val eventBus = BroadcastEventBus<TrackEvent>()
val events = eventBus.readOnly()
var name = name
internal set
var kind = kind
... ... @@ -72,7 +77,7 @@ open class Track(
}
}
data class Dimensions(var width: Int, var height: Int)
data class Dimensions(val width: Int, val height: Int)
open fun start() {
rtcTrack.setEnabled(true)
... ... @@ -83,6 +88,7 @@ open class Track(
}
}
sealed class TrackException(message: String? = null, cause: Throwable? = null) :
Exception(message, cause) {
class InvalidTrackTypeException(message: String? = null, cause: Throwable? = null) :
... ...
... ... @@ -9,7 +9,7 @@ open class TrackPublication(
track: Track?,
participant: Participant
) {
var track: Track? = track
open var track: Track? = track
internal set
var name: String
internal set
... ...
... ... @@ -5,7 +5,7 @@ import org.webrtc.VideoTrack
open class VideoTrack(name: String, override val rtcTrack: VideoTrack) :
Track(name, Kind.VIDEO, rtcTrack) {
internal val sinks: MutableList<VideoSink> = ArrayList();
protected val sinks: MutableList<VideoSink> = ArrayList();
var enabled: Boolean
get() = rtcTrack.enabled()
... ... @@ -13,12 +13,12 @@ open class VideoTrack(name: String, override val rtcTrack: VideoTrack) :
rtcTrack.setEnabled(value)
}
fun addRenderer(renderer: VideoSink) {
open fun addRenderer(renderer: VideoSink) {
sinks.add(renderer)
rtcTrack.addSink(renderer)
}
fun removeRenderer(renderer: VideoSink) {
open fun removeRenderer(renderer: VideoSink) {
rtcTrack.removeSink(renderer)
sinks.remove(renderer)
}
... ...
package io.livekit.android.room.track.video
import android.graphics.Rect
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.CallSuper
import androidx.compose.ui.layout.LayoutCoordinates
import io.livekit.android.room.track.Track
import java.util.*
abstract class VideoSinkVisibility : Observable() {
abstract fun isVisible(): Boolean
abstract fun size(): Track.Dimensions
/**
* This should be called whenever the visibility or size has changed.
*/
fun notifyChanged() {
setChanged()
notifyObservers()
}
/**
* Called when this object is no longer needed and should clean up any unused resources.
*/
@CallSuper
open fun close() {
deleteObservers()
}
}
class ComposeVisibility : VideoSinkVisibility() {
private var lastCoordinates: LayoutCoordinates? = null
override fun isVisible(): Boolean {
return (lastCoordinates?.isAttached == true && lastCoordinates?.size?.width != 0 && lastCoordinates?.size?.height != 0)
}
override fun size(): Track.Dimensions {
val width = lastCoordinates?.size?.width ?: 0
val height = lastCoordinates?.size?.height ?: 0
return Track.Dimensions(width, height)
}
fun onGloballyPositioned(layoutCoordinates: LayoutCoordinates) {
val lastVisible = isVisible()
val lastSize = size()
lastCoordinates = layoutCoordinates
if (lastVisible != isVisible() || lastSize != size()) {
notifyChanged()
}
}
fun onDispose() {
if (lastCoordinates == null) {
return
}
lastCoordinates = null
notifyChanged()
}
}
class ViewVisibility(private val view: View) : VideoSinkVisibility() {
private val handler = Handler(Looper.getMainLooper())
private val globalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
val lastVisibility = false
val lastSize = Track.Dimensions(0, 0)
override fun onGlobalLayout() {
handler.removeCallbacksAndMessages(null)
handler.postDelayed({
var shouldNotify = false
val newVisibility = isVisible()
val newSize = size()
if (newVisibility != lastVisibility) {
shouldNotify = true
}
if (newSize != lastSize) {
shouldNotify = true
}
if (shouldNotify) {
notifyChanged()
}
}, 2000)
}
}
init {
view.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
}
private val loc = IntArray(2)
private val viewRect = Rect()
private val windowRect = Rect()
private fun isViewAncestorsVisible(view: View): Boolean {
if (view.visibility != View.VISIBLE) {
return false
}
val parent = view.parent as? View
if (parent != null) {
return isViewAncestorsVisible(parent)
}
return true
}
override fun isVisible(): Boolean {
if (view.windowVisibility != View.VISIBLE || !isViewAncestorsVisible(view)) {
return false
}
view.getLocationInWindow(loc)
viewRect.set(loc[0], loc[1], loc[0] + view.width, loc[1] + view.height)
view.getWindowVisibleDisplayFrame(windowRect)
// Ensure window rect origin is at 0,0
windowRect.offset(-windowRect.left, -windowRect.top)
return viewRect.intersect(windowRect)
}
override fun size(): Track.Dimensions {
return Track.Dimensions(view.width, view.height)
}
override fun close() {
super.close()
handler.removeCallbacksAndMessages(null)
view.viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
}
}
\ No newline at end of file
... ...
... ... @@ -15,4 +15,8 @@ fun <T, R> debounce(
return@async destinationFunction(param)
}
}
}
fun <R> ((Unit) -> R).invoke() {
this.invoke(Unit)
}
\ No newline at end of file
... ...
... ... @@ -55,7 +55,8 @@ class RoomTest {
rtcEngine,
eglBase,
localParticantFactory,
DefaultsManager()
DefaultsManager(),
coroutineRule.dispatcher
)
}
... ...
package io.livekit.android.room.participant
import io.livekit.android.coroutines.TestCoroutineRule
import io.livekit.android.room.SignalClient
import livekit.LivekitModels
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
class RemoteParticipantTest {
@get:Rule
var coroutineRule = TestCoroutineRule()
lateinit var signalClient: SignalClient
lateinit var participant: RemoteParticipant
@Before
fun setup() {
signalClient = Mockito.mock(SignalClient::class.java)
participant = RemoteParticipant(signalClient, "sid")
participant = RemoteParticipant(
"sid",
signalClient = signalClient,
ioDispatcher = coroutineRule.dispatcher
)
}
@Test
... ... @@ -24,7 +33,7 @@ class RemoteParticipantTest {
.addTracks(TRACK_INFO)
.build()
participant = RemoteParticipant(signalClient, info)
participant = RemoteParticipant(info, signalClient, ioDispatcher = coroutineRule.dispatcher)
assertEquals(1, participant.tracks.values.size)
assertNotNull(participant.getTrackPublication(TRACK_INFO.sid))
... ...
... ... @@ -46,7 +46,9 @@ class CallViewModel(
application,
url,
token,
ConnectOptions(),
ConnectOptions(
autoManageVideo = true,
),
this@CallViewModel
)
... ...
... ... @@ -3,6 +3,7 @@ package io.livekit.android.composesample
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.viewinterop.AndroidView
import com.github.ajalt.timberkt.Timber
import io.livekit.android.renderer.TextureViewRenderer
... ... @@ -10,39 +11,46 @@ import io.livekit.android.room.Room
import io.livekit.android.room.participant.ParticipantListener
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.RemoteTrackPublication
import io.livekit.android.room.track.RemoteVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.VideoTrack
import io.livekit.android.room.track.video.ComposeVisibility
@Composable
fun ParticipantItem(
room: Room,
participant: RemoteParticipant,
) {
val videoSinkVisibility = remember(room, participant) { ComposeVisibility() }
var videoBound by remember(room, participant) { mutableStateOf(false) }
fun getVideoTrack(): VideoTrack? {
fun getVideoTrack(): RemoteVideoTrack? {
return participant
.videoTracks.values
.firstOrNull()?.track as? VideoTrack
.firstOrNull()?.track as? RemoteVideoTrack
}
fun setupVideoIfNeeded(videoTrack: VideoTrack, view: TextureViewRenderer) {
fun setupVideoIfNeeded(videoTrack: RemoteVideoTrack, view: TextureViewRenderer) {
if (videoBound) {
return
}
videoBound = true
Timber.v { "adding renderer to $videoTrack" }
videoTrack.addRenderer(view)
videoTrack.addRenderer(view, videoSinkVisibility)
}
DisposableEffect(room, participant) {
onDispose {
videoSinkVisibility.onDispose()
}
}
AndroidView(
factory = { context ->
TextureViewRenderer(context).apply {
room.initVideoRenderer(this)
}
},
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.onGloballyPositioned { videoSinkVisibility.onGloballyPositioned(it) },
update = { view ->
participant.listener = object : ParticipantListener {
override fun onTrackSubscribed(
... ... @@ -50,7 +58,7 @@ fun ParticipantItem(
publication: RemoteTrackPublication,
participant: RemoteParticipant
) {
if (track is VideoTrack) {
if (track is RemoteVideoTrack) {
setupVideoIfNeeded(track, view)
}
}
... ...