Skip to content

Commit

Permalink
Add track reference methods
Browse files Browse the repository at this point in the history
  • Loading branch information
davidliu committed Oct 2, 2023
1 parent 3429170 commit 0530dc8
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 25 deletions.
2 changes: 1 addition & 1 deletion livekit-compose-components/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ dokkaHtml {
}

dependencies {
api "io.livekit:livekit-android:1.4.1"
api "io.livekit:livekit-android:1.4.3-SNAPSHOT"

implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:${versions.serialization}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,27 +88,3 @@ fun rememberVideoTrackPublication(

return trackPubState.value
}

/**
* Observes the [videoPub] object for the track.
*
* A track publication will only have the track when it is subscribed,
* so this ensures the composition is updated with the correct track value
* as needed.
*/
@Composable
fun rememberVideoTrack(videoPub: TrackPublication?): VideoTrack? {
val trackState = remember { mutableStateOf<VideoTrack?>(null) }

LaunchedEffect(videoPub) {
if (videoPub == null) {
trackState.value = null
} else {
videoPub::track.flow.collectLatest { track ->
trackState.value = track as? VideoTrack
}
}
}

return trackState.value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.livekit.android.compose.sorting

import io.livekit.android.compose.types.TrackReference
import io.livekit.android.room.participant.LocalParticipant
import io.livekit.android.room.track.Track

/**
* Default sort for a list of [TrackReference]. Orders by:
*
* 1. local camera track (publication.isLocal)
* 2. remote screen_share track
* 3. local screen_share track
* 4. remote dominant speaker camera track (sorted by speaker with the loudest audio level)
* 5. other remote speakers that are recently active
* 6. remote unmuted camera tracks
* 7. remote tracks sorted by joinedAt
*/
fun sortTrackReferences(trackRefs: List<TrackReference>): List<TrackReference> {

val localTracks = mutableListOf<TrackReference>()
val screenShareTracks = mutableListOf<TrackReference>()
val cameraTracks = mutableListOf<TrackReference>()
val undefinedTracks = mutableListOf<TrackReference>()

trackRefs.forEach { trackRef ->
if (trackRef.participant is LocalParticipant && trackRef.source == Track.Source.CAMERA) {
localTracks.add(trackRef)
} else if (trackRef.source == Track.Source.SCREEN_SHARE) {
screenShareTracks.add(trackRef)
} else if (trackRef.source == Track.Source.CAMERA) {
cameraTracks.add(trackRef)
} else {
undefinedTracks.add(trackRef)
}
}

val sortedScreenShareTracks = sortScreenShareTracks(screenShareTracks)
val sortedCameraTracks = sortCameraTracks(cameraTracks)

return localTracks
.plus(sortedScreenShareTracks)
.plus(sortedCameraTracks)
.plus(undefinedTracks)
}

/**
* Sort an array of screen share [TrackReference].
* Main sorting order:
* 1. remote screen shares
* 2. local screen shares
* Secondary sorting by participant's joining time.
*/
private fun sortScreenShareTracks(screenShareTracks: List<TrackReference>): List<TrackReference> {
val localScreenShares = screenShareTracks.filter { it.participant is LocalParticipant }
val remoteScreenShares = screenShareTracks.filter { it.participant !is LocalParticipant }
.sortedBy { it.participant.joinedAt }

return localScreenShares.plus(remoteScreenShares)
}

/**
* Sort an array of camera [TrackReference].
*/
private fun sortCameraTracks(cameraTracks: List<TrackReference>): List<TrackReference> {

return cameraTracks.sortedWith { a, b ->
// Participant with higher audio level goes first.
if (a.participant.isSpeaking && b.participant.isSpeaking) {
return@sortedWith compareAudioLevel(a.participant, b.participant);
}

// A speaking participant goes before one that is not speaking.
if (a.participant.isSpeaking != b.participant.isSpeaking) {
return@sortedWith compareIsSpeaking(a.participant, b.participant);
}

// A participant that spoke recently goes before a participant that spoke a while back.
if (a.participant.lastSpokeAt != b.participant.lastSpokeAt) {
return@sortedWith compareLastSpokenAt(a.participant, b.participant);
}

// TrackReference before TrackReferencePlaceholder
if (a.isPlaceholder() != b.isPlaceholder()) {
return@sortedWith compareTrackReferencesByPlaceHolder(a, b);
}

// Tiles with video on before tiles with muted video track.
if (a.isEnabled() != b.isEnabled()) {
return@sortedWith compareTrackReferencesByIsEnabled(a, b);
}

// A participant that joined a long time ago goes before one that joined recently.
return@sortedWith compareJoinedAt(a.participant, b.participant);
}
}

private fun TrackReference.isEnabled() = (publication?.subscribed ?: false) && !(publication?.muted ?: true)

fun compareTrackReferencesByPlaceHolder(a: TrackReference, b: TrackReference): Int {
return compareValues(a.isPlaceholder(), b.isPlaceholder())
}

fun compareTrackReferencesByIsEnabled(a: TrackReference, b: TrackReference): Int {
return compareValues(b.isEnabled(), a.isEnabled())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.livekit.android.compose.state

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import io.livekit.android.compose.types.TrackIdentifier
import io.livekit.android.compose.types.TrackReference
import io.livekit.android.compose.types.TrackSource
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackPublication
import io.livekit.android.util.flow
import kotlinx.coroutines.flow.collectLatest

/**
* Observes the [trackPublication] object for the track.
*
* A track publication will only have the track when it is subscribed,
* so this ensures the composition is updated with the correct track value
* as needed.
*/
@Composable
fun <T : Track> rememberTrack(trackPublication: TrackPublication?): T? {
val trackState = remember { mutableStateOf<T?>(null) }

LaunchedEffect(trackPublication) {
if (trackPublication == null) {
trackState.value = null
} else {
trackPublication::track.flow.collectLatest { track ->
@Suppress("UNCHECKED_CAST")
trackState.value = track as? T
}
}
}

return trackState.value
}

/**
* Observes the [trackIdentifier] object for the track.
*
* A track publication will only have the track when it is subscribed,
* so this ensures the composition is updated with the correct track value
* as needed.
*
* @see TrackSource
* @see TrackReference
*/
@Composable
fun <T : Track> rememberTrack(trackIdentifier: TrackIdentifier): T? {
return rememberTrack(trackIdentifier.getTrackPublication())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.livekit.android.compose.state

import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import io.livekit.android.compose.local.RoomLocal
import io.livekit.android.compose.local.requireRoom
import io.livekit.android.compose.types.TrackReference
import io.livekit.android.events.RoomEvent
import io.livekit.android.room.Room
import io.livekit.android.room.track.Track
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map

/**
* Returns an array of TrackReferences depending the sources provided.
*
* @param sources The sources of the tracks to provide. Defaults to all tracks.
* @param usePlaceholders A set of sources to provide placeholders for.
* A placeholder will provide a TrackReference for participants that don't
* yet have a track published for that source. Defaults to no placeholders.
* @param passedRoom The room to use on, or [RoomLocal] if null.
* @param updateOn Room events to listen to. Defaults to all events.
* @param onlySubscribed If true, only return tracks that have been subscribed. Defaults to true.
*/
@Composable
fun rememberTrackReferences(
sources: List<Track.Source> = listOf(
Track.Source.CAMERA,
Track.Source.MICROPHONE,
Track.Source.SCREEN_SHARE,
Track.Source.UNKNOWN
),
usePlaceholders: Set<Track.Source> = emptySet(),
passedRoom: Room? = null,
updateOn: Set<Class<RoomEvent>>? = null,
onlySubscribed: Boolean = true,
): State<List<TrackReference>> {
val room = requireRoom(passedRoom)

return trackReferencesFlow(
room = room,
sources = sources,
usePlaceholders = usePlaceholders,
updateOn = updateOn,
onlySubscribed = onlySubscribed
).collectAsState(initial = room.getTrackReferences(sources, usePlaceholders, onlySubscribed))
}

fun trackReferencesFlow(
room: Room,
sources: List<Track.Source>,
usePlaceholders: Set<Track.Source> = emptySet(),
updateOn: Set<Class<RoomEvent>>? = null,
onlySubscribed: Boolean = true,
): Flow<List<TrackReference>> {
return room.events.events
.filter { updateOn == null || updateOn.contains(it::class.java) }
.map { room.getTrackReferences(sources, usePlaceholders, onlySubscribed) }
}

fun Room.getTrackReferences(
sources: List<Track.Source>,
usePlaceholders: Set<Track.Source> = emptySet(),
onlySubscribed: Boolean = true
): List<TrackReference> {
val allParticipants = listOf(localParticipant).plus(remoteParticipants.values)
return allParticipants.flatMap { participant ->
sources.map { source ->
var tracks = participant.tracks.values.mapNotNull { trackPub ->
if (trackPub.source == source &&
(!onlySubscribed || trackPub.subscribed)
) {
TrackReference(
participant = participant,
publication = trackPub,
source = trackPub.source
)
} else {
null
}
}
if (tracks.isEmpty() && usePlaceholders.contains(source)) {
// Add placeholder
tracks = listOf(
TrackReference(
participant = participant,
publication = null,
source = source,
)
)
}
return@flatMap tracks
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.livekit.android.compose.types

import io.livekit.android.room.participant.Participant
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackPublication

interface TrackIdentifier {
val participant: Participant

fun getTrackPublication(): TrackPublication?
}

/**
* Identifies a track based on the source and/or name. At least one is required.
*/
data class TrackSource(
override val participant: Participant,
val source: Track.Source? = null,
val name: String? = null,
) : TrackIdentifier {
init {
require(source != null || name != null) { "At least one of source or name must be provided!" }
}

override fun getTrackPublication(): TrackPublication? {
return if (source != null && name != null) {
participant.tracks.values
.firstOrNull { p -> p.source == source && p.name == name }
} else if (source != null) {
participant.getTrackPublication(source)
} else if (name != null) {
participant.getTrackPublicationByName(name)
} else {
throw IllegalStateException("At least one of source or name must be provided!")
}
}
}

class TrackReference(
override val participant: Participant,
val publication: TrackPublication?,
val source: Track.Source,
) : TrackIdentifier {
override fun getTrackPublication(): TrackPublication? {
return publication
}

fun isPlaceholder(): Boolean {
return publication == null
}
}
3 changes: 3 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven { url 'https://jitpack.io' }

// For SNAPSHOT access
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
}
}
rootProject.name = "components-android"
Expand Down

0 comments on commit 0530dc8

Please sign in to comment.