Skip to content

Commit

Permalink
Transcription hooks (#25)
Browse files Browse the repository at this point in the history
* save

* rememberTranscriptions hooks

* Use derivedstateof for flow creation

* fix

* spotless
  • Loading branch information
davidliu authored Sep 12, 2024
1 parent eeba4f0 commit 5e5c473
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 4 deletions.
2 changes: 1 addition & 1 deletion livekit-compose-components/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ dokkaHtml {
}
}

var livekitVersion = "2.2.0"
var livekitVersion = "2.8.1"
dependencies {
api "io.livekit:livekit-android:${livekitVersion}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ package io.livekit.android.compose.flow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import io.livekit.android.events.EventListenable
import io.livekit.android.events.ParticipantEvent
import io.livekit.android.events.RoomEvent
import io.livekit.android.events.TrackEvent
import io.livekit.android.events.TrackPublicationEvent
import io.livekit.android.events.collect
import io.livekit.android.room.Room
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackPublication
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow

Expand All @@ -37,12 +44,63 @@ import kotlinx.coroutines.flow.MutableSharedFlow
*/
@Composable
inline fun <reified T : RoomEvent> rememberEventSelector(room: Room): Flow<T> {
val flow = remember(room) {
return rememberEventSelector<T, RoomEvent>(eventListenable = room.events)
}

/**
* A utility method to obtain a flow for specific room events.
*
* Pass in [ParticipantEvent] as the type to receive all room events.
*
* ```
* // Receive only participant connected events.
* val eventFlow = rememberEventSelector<ParticipantEvent.SpeakingChanged>(participant)
* ```
*/
@Composable
inline fun <reified T : ParticipantEvent> rememberEventSelector(participant: Participant): Flow<T> {
return rememberEventSelector<T, ParticipantEvent>(eventListenable = participant.events)
}

/**
* A utility method to obtain a flow for specific room events.
*
* Pass in [TrackPublicationEvent] as the type to receive all publication events.
*
* ```
* // Receive only participant connected events.
* val eventFlow = rememberEventSelector<TrackPublicationEvent.TranscriptionReceived>(publication)
* ```
*/
@Composable
inline fun <reified T : TrackPublicationEvent> rememberEventSelector(publication: TrackPublication): Flow<T> {
return rememberEventSelector<T, TrackPublicationEvent>(eventListenable = publication.events)
}

/**
* A utility method to obtain a flow for specific track events.
*
* Pass in [TrackEvent] as the type to receive all track events.
*
* ```
* // Receive only stream state changed events.
* val eventFlow = rememberEventSelector<TrackEvent.StreamStateChanged>(track)
* ```
*/
@Composable
inline fun <reified T : TrackEvent> rememberEventSelector(track: Track): Flow<T> {
return rememberEventSelector<T, TrackEvent>(eventListenable = track.events)
}

@Composable
inline fun <reified T, U> rememberEventSelector(eventListenable: EventListenable<U>): Flow<T>
where T : U {
val flow = remember(eventListenable) {
MutableSharedFlow<T>(extraBufferCapacity = 100)
}

LaunchedEffect(room) {
room.events.collect {
LaunchedEffect(eventListenable) {
eventListenable.collect {
if (it is T) {
flow.emit(it)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.livekit.android.compose.state.transcriptions

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import io.livekit.android.annotations.Beta
import io.livekit.android.compose.flow.rememberEventSelector
import io.livekit.android.compose.local.requireParticipant
import io.livekit.android.compose.local.requireRoom
import io.livekit.android.compose.types.TrackReference
import io.livekit.android.events.ParticipantEvent
import io.livekit.android.events.RoomEvent
import io.livekit.android.events.TrackPublicationEvent
import io.livekit.android.room.Room
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.types.TranscriptionSegment
import io.livekit.android.room.types.mergeNewSegments
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* Collect all the transcriptions for the room.
*
* @return Returns the collected transcriptions, ordered by [TranscriptionSegment.firstReceivedTime].
*/
@Beta
@Composable
fun rememberTranscriptions(passedRoom: Room? = null): List<TranscriptionSegment> {
val room = requireRoom(passedRoom)
val events = rememberEventSelector<RoomEvent.TranscriptionReceived>(room)
val flow by remember(events) {
derivedStateOf {
events.map { it.transcriptionSegments }
}
}

return rememberTranscriptionsImpl(transcriptionsFlow = flow)
}

/**
* Collect all the transcriptions for a track reference.
*
* @return Returns the collected transcriptions, ordered by [TranscriptionSegment.firstReceivedTime].
*/
@Beta
@Composable
fun rememberTrackTranscriptions(trackReference: TrackReference): List<TranscriptionSegment> {
val publication = trackReference.publication ?: return emptyList()
val events = rememberEventSelector<TrackPublicationEvent.TranscriptionReceived>(publication)
val flow by remember(events) {
derivedStateOf {
events.map { it.transcriptions }
}
}

return rememberTranscriptionsImpl(transcriptionsFlow = flow)
}

/**
* Collect all the transcriptions for a participant.
*
* @return Returns the collected transcriptions, ordered by [TranscriptionSegment.firstReceivedTime].
*/
@Beta
@Composable
fun rememberParticipantTranscriptions(passedParticipant: Participant? = null): List<TranscriptionSegment> {
val participant = requireParticipant(passedParticipant)
val events = rememberEventSelector<ParticipantEvent.TranscriptionReceived>(participant)
val flow by remember(events) {
derivedStateOf {
events.map { it.transcriptions }
}
}

return rememberTranscriptionsImpl(transcriptionsFlow = flow)
}

@Composable
internal fun rememberTranscriptionsImpl(transcriptionsFlow: Flow<List<TranscriptionSegment>>): List<TranscriptionSegment> {
val segments = remember(transcriptionsFlow) { mutableStateMapOf<String, TranscriptionSegment>() }
val orderedSegments = remember(segments) {
derivedStateOf {
segments.values.sortedBy { segment -> segment.firstReceivedTime }
}
}
LaunchedEffect(transcriptionsFlow) {
transcriptionsFlow.collect {
segments.mergeNewSegments(it)
}
}

return orderedSegments.value
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.livekit.android.compose.flow.rememberEventSelector
import io.livekit.android.events.RoomEvent
import io.livekit.android.test.MockE2ETest
import io.livekit.android.test.assert.assertIsClass
import io.livekit.android.test.mock.TestData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import org.junit.Test
Expand All @@ -47,4 +48,24 @@ class RememberEventSelectorTest : MockE2ETest() {
connect()
job.join()
}

@Test
fun getsEventsFiltered() = runTest {
val job = coroutineRule.scope.launch {
moleculeFlow(RecompositionClock.Immediate) {
rememberEventSelector<RoomEvent.ParticipantConnected>(room = room).collectAsState(initial = null).value
}.test {
// discard initial state.
awaitItem()

// Connected event should be skipped and only the remote participant connection event should be emitted.
val event = awaitItem()
assertIsClass(RoomEvent.ParticipantConnected::class.java, event)
ensureAllEventsConsumed()
}
}
connect()
wsFactory.receiveMessage(TestData.PARTICIPANT_JOIN)
job.join()
}
}

0 comments on commit 5e5c473

Please sign in to comment.