-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
310 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
...ompose-components/src/main/java/io/livekit/android/compose/sorting/SortTrackReferences.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
53 changes: 53 additions & 0 deletions
53
livekit-compose-components/src/main/java/io/livekit/android/compose/state/RememberTrack.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
97 changes: 97 additions & 0 deletions
97
...pose-components/src/main/java/io/livekit/android/compose/state/RememberTrackReferences.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
livekit-compose-components/src/main/java/io/livekit/android/compose/types/TrackIdentifier.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters