Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transcription hooks #25

Merged
merged 5 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}
}
Loading