feat: continue voice impl
This commit is contained in:
parent
623ace3998
commit
10efd73965
|
|
@ -258,9 +258,9 @@ dependencies {
|
|||
implementation(libs.jetbrains.markdown)
|
||||
implementation(libs.highlights)
|
||||
|
||||
// implementation(libs.livekit.android)
|
||||
// implementation(libs.livekit.android.camerax)
|
||||
// implementation(libs.livekit.android.compose)
|
||||
implementation(libs.livekit.android)
|
||||
implementation(libs.livekit.android.camerax)
|
||||
implementation(libs.livekit.android.compose)
|
||||
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.messaging)
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ import chat.stoat.api.settings.LoadedSettings
|
|||
import chat.stoat.api.settings.SyncedSettings
|
||||
import chat.stoat.composables.generic.HealthAlert
|
||||
import chat.stoat.composables.voice.VoicePermissionSwitch
|
||||
import chat.stoat.composables.voice.VoiceSheet
|
||||
import chat.stoat.core.model.schemas.HealthNotice
|
||||
import chat.stoat.material.EasingTokens
|
||||
import chat.stoat.ndk.NativeLibraries
|
||||
|
|
@ -791,18 +792,14 @@ fun AppEntrypoint(
|
|||
showVoiceUI = false
|
||||
}
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
showVoiceUI = false
|
||||
voiceChannelId = null
|
||||
}
|
||||
voiceChannelId?.let {
|
||||
/*VoiceSheet(
|
||||
VoiceSheet(
|
||||
it,
|
||||
onDisconnect = {
|
||||
showVoiceUI = false
|
||||
voiceChannelId = null
|
||||
}
|
||||
)*/
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import io.ktor.http.ContentType
|
|||
import io.ktor.http.contentType
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
@Serializable
|
||||
data class JoinCallResponse(
|
||||
|
|
@ -18,10 +20,19 @@ data class JoinCallResponse(
|
|||
val url: String,
|
||||
)
|
||||
|
||||
suspend fun joinCall(channelId: String, nodeName: String): JoinCallResponse {
|
||||
suspend fun joinCall(
|
||||
channelId: String,
|
||||
nodeName: String,
|
||||
forceDisconnect: Boolean = true
|
||||
): JoinCallResponse {
|
||||
val response = StoatHttp.post("/channels/$channelId/join_call".api()) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapOf("node" to nodeName))
|
||||
setBody(
|
||||
buildJsonObject {
|
||||
put("node", nodeName)
|
||||
put("force_disconnect", forceDisconnect)
|
||||
}
|
||||
)
|
||||
}.bodyAsText()
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,28 @@
|
|||
package chat.stoat.composables.voice
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.stoat.R
|
||||
import chat.stoat.api.StoatAPI
|
||||
import chat.stoat.core.model.util.UserVoiceState
|
||||
import chat.stoat.composables.chat.displayNameInChannel
|
||||
import chat.stoat.composables.generic.UserAvatar
|
||||
import chat.stoat.core.model.util.UserVoiceState
|
||||
import chat.stoat.internals.extensions.TransparentListItemColours
|
||||
|
||||
@Composable
|
||||
|
|
@ -23,23 +32,40 @@ fun VoiceParticipant(
|
|||
speaking: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val speakingBorderColour by animateColorAsState(
|
||||
targetValue = if (speaking) MaterialTheme.colorScheme.primary else Color.Transparent,
|
||||
animationSpec = spring()
|
||||
)
|
||||
val speakingBorderWidth by animateDpAsState(
|
||||
targetValue = if (speaking) 2.dp else 0.dp,
|
||||
animationSpec = spring()
|
||||
)
|
||||
|
||||
val user = StoatAPI.userCache[state.id]
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
colors = TransparentListItemColours,
|
||||
headlineContent = {
|
||||
Text(displayNameInChannel(state.id, channelId))
|
||||
},
|
||||
leadingContent = {
|
||||
// TODO circle when speaking
|
||||
UserAvatar(
|
||||
username = displayNameInChannel(state.id, channelId),
|
||||
userId = state.id,
|
||||
allowAnimation = speaking,
|
||||
avatar = user?.avatar
|
||||
avatar = user?.avatar,
|
||||
modifier = Modifier.border(
|
||||
speakingBorderWidth,
|
||||
speakingBorderColour,
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Row {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
) {
|
||||
if (!state.isPublishing) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_mic_off_24dp),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
package chat.stoat.composables.voice
|
||||
/*
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
|
@ -24,8 +31,11 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -46,6 +56,9 @@ import io.livekit.android.compose.local.RoomScope
|
|||
import io.livekit.android.compose.state.rememberTracks
|
||||
import io.livekit.android.compose.ui.VideoTrackView
|
||||
import io.livekit.android.room.Room
|
||||
import io.livekit.android.util.flow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import logcat.asLog
|
||||
import logcat.logcat
|
||||
|
|
@ -124,6 +137,8 @@ fun VoiceSheet(
|
|||
viewModel.getVoiceToken()
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
RoomScope(
|
||||
url = viewModel.voiceLkNode,
|
||||
token = viewModel.voiceToken,
|
||||
|
|
@ -132,10 +147,22 @@ fun VoiceSheet(
|
|||
connect = true,
|
||||
) {
|
||||
val room = RoomLocal.current
|
||||
val trackRefs = rememberTracks()
|
||||
val roomState by room::state.flow.collectAsState()
|
||||
val isMicOn by room.localParticipant::isMicrophoneEnabled.flow.collectAsState()
|
||||
val isCameraOn by room.localParticipant::isCameraEnabled.flow.collectAsState()
|
||||
val isScreenShared by room.localParticipant::isScreenShareEnabled.flow.collectAsState()
|
||||
val activeSpeakers by room::activeSpeakers.flow.collectAsState()
|
||||
val trackRefs by rememberTracks(passedRoom = room)
|
||||
|
||||
Column {
|
||||
LazyColumn(modifier = Modifier.animateContentSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.animateContentSize(
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
)
|
||||
) {
|
||||
val voiceStates = StoatAPI.voiceStateCache[viewModel.channelId]
|
||||
items(voiceStates?.participants?.size ?: 0) { index ->
|
||||
val participantState = voiceStates?.participants[index]
|
||||
|
|
@ -143,56 +170,104 @@ fun VoiceSheet(
|
|||
VoiceParticipant(
|
||||
state = participantState,
|
||||
channelId = viewModel.channelId,
|
||||
speaking = false
|
||||
speaking = activeSpeakers.any { it.identity?.value == participantState.id }
|
||||
)
|
||||
}
|
||||
}
|
||||
items(trackRefs.size) { index ->
|
||||
VideoTrackView(
|
||||
trackReference = trackRefs[index],
|
||||
room = room,
|
||||
modifier = Modifier.fillParentMaxHeight(0.5f)
|
||||
)
|
||||
}
|
||||
item(key = "status") {
|
||||
AnimatedContent(
|
||||
room.state
|
||||
) { roomState ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
8.dp,
|
||||
Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides when (roomState) {
|
||||
Room.State.CONNECTING, Room.State.RECONNECTING -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Room.State.CONNECTED -> MaterialTheme.colorScheme.primary
|
||||
Room.State.DISCONNECTED -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
var showStatus by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(roomState) {
|
||||
if (roomState == Room.State.CONNECTED) {
|
||||
delay(1000)
|
||||
showStatus = false
|
||||
} else {
|
||||
showStatus = true
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showStatus,
|
||||
enter = fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
) +
|
||||
expandVertically(
|
||||
expandFrom = Alignment.Top,
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
),
|
||||
exit = fadeOut(
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
) +
|
||||
slideOutVertically(
|
||||
targetOffsetY = { it },
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
) +
|
||||
shrinkVertically(
|
||||
shrinkTowards = Alignment.Top,
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
)
|
||||
) {
|
||||
AnimatedContent(
|
||||
roomState
|
||||
) { roomState ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
8.dp,
|
||||
Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
when (roomState) {
|
||||
Room.State.CONNECTING -> R.drawable.ic_sprint_24dp
|
||||
Room.State.CONNECTED -> R.drawable.ic_wifi_tethering_24dp
|
||||
Room.State.DISCONNECTED -> R.drawable.ic_wifi_tethering_error_24dp
|
||||
Room.State.RECONNECTING -> R.drawable.ic_sprint_24dp
|
||||
}
|
||||
),
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = when (roomState) {
|
||||
Room.State.CONNECTING -> stringResource(R.string.voice_status_connecting)
|
||||
Room.State.CONNECTED -> stringResource(R.string.voice_status_connected)
|
||||
Room.State.DISCONNECTED -> stringResource(R.string.voice_status_disconnected)
|
||||
Room.State.RECONNECTING -> stringResource(R.string.voice_status_reconnecting)
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides when (roomState) {
|
||||
Room.State.CONNECTING, Room.State.RECONNECTING -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Room.State.CONNECTED -> MaterialTheme.colorScheme.primary
|
||||
Room.State.DISCONNECTED -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
when (roomState) {
|
||||
Room.State.CONNECTING -> R.drawable.ic_sprint_24dp
|
||||
Room.State.CONNECTED -> R.drawable.ic_wifi_tethering_24dp
|
||||
Room.State.DISCONNECTED -> R.drawable.ic_wifi_tethering_error_24dp
|
||||
Room.State.RECONNECTING -> R.drawable.ic_sprint_24dp
|
||||
}
|
||||
),
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = when (roomState) {
|
||||
Room.State.CONNECTING -> stringResource(R.string.voice_status_connecting)
|
||||
Room.State.CONNECTED -> stringResource(R.string.voice_status_connected)
|
||||
Room.State.DISCONNECTED -> stringResource(R.string.voice_status_disconnected)
|
||||
Room.State.RECONNECTING -> stringResource(R.string.voice_status_reconnecting)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -226,10 +301,13 @@ fun VoiceSheet(
|
|||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
room.localParticipant.setMicrophoneEnabled(!isMicOn)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
containerColor = if (isMicOn) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = if (isMicOn) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
shapes = ButtonDefaults.shapes(),
|
||||
modifier = Modifier
|
||||
|
|
@ -237,17 +315,23 @@ fun VoiceSheet(
|
|||
.height(64.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_mic_off_24dp),
|
||||
painter = if (isMicOn) painterResource(R.drawable.ic_mic_24dp) else painterResource(
|
||||
R.drawable.ic_mic_off_24dp
|
||||
),
|
||||
contentDescription = "TODO change this string to res"
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
room.localParticipant.setCameraEnabled(!isMicOn)
|
||||
}
|
||||
},
|
||||
enabled = false,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
containerColor = if (isCameraOn) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = if (isCameraOn) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
shapes = ButtonDefaults.shapes(),
|
||||
modifier = Modifier
|
||||
|
|
@ -255,14 +339,20 @@ fun VoiceSheet(
|
|||
.height(64.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_videocam_off_24dp),
|
||||
painter = if (isCameraOn) painterResource(R.drawable.ic_videocam_24dp) else painterResource(
|
||||
R.drawable.ic_videocam_off_24dp
|
||||
),
|
||||
contentDescription = "TODO change this string to res"
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
room.localParticipant.setScreenShareEnabled(!isScreenShared)
|
||||
}
|
||||
},
|
||||
enabled = false,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
|
|
@ -280,7 +370,6 @@ fun VoiceSheet(
|
|||
Spacer(Modifier.width(4.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
room.disconnect()
|
||||
onDisconnect()
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
|
|
@ -300,4 +389,4 @@ fun VoiceSheet(
|
|||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import chat.stoat.api.internals.Roles
|
|||
@Composable
|
||||
fun rememberChannelPermissions(channelId: String, key1: Any = Unit): MutableLongState {
|
||||
val permissions = rememberSaveable { mutableLongStateOf(0L) }
|
||||
val selfId = StoatAPI.selfId
|
||||
val channel = StoatAPI.channelCache[channelId]
|
||||
|
||||
LaunchedEffect(channelId, key1) {
|
||||
LaunchedEffect(channelId, key1, selfId, channel) {
|
||||
if (StoatAPI.selfId == null) return@LaunchedEffect
|
||||
if (StoatAPI.userCache[StoatAPI.selfId] == null) return@LaunchedEffect
|
||||
if (StoatAPI.channelCache[channelId] == null) return@LaunchedEffect
|
||||
|
|
|
|||
|
|
@ -952,7 +952,7 @@ fun ChannelScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (viewModel.channel?.voice != null &&
|
||||
if ((viewModel.channel?.channelType == ChannelType.VoiceChannel || viewModel.channel?.voice != null) &&
|
||||
channelPermissions has PermissionBit.Connect &&
|
||||
Experiments.useVoiceChats2p0.isEnabled
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ import chat.stoat.api.routes.microservices.autumn.uploadToAutumn
|
|||
import chat.stoat.api.routes.server.fetchMember
|
||||
import chat.stoat.api.routes.user.addUserIfUnknown
|
||||
import chat.stoat.api.routes.user.fetchUser
|
||||
import chat.stoat.core.model.schemas.Channel
|
||||
import chat.stoat.core.model.schemas.Message
|
||||
import chat.stoat.api.settings.GeoStateProvider
|
||||
import chat.stoat.callbacks.Action
|
||||
import chat.stoat.callbacks.ActionChannel
|
||||
import chat.stoat.callbacks.UiCallback
|
||||
import chat.stoat.callbacks.UiCallbacks
|
||||
import chat.stoat.core.model.schemas.Channel
|
||||
import chat.stoat.core.model.schemas.Message
|
||||
import chat.stoat.internals.text.MessageProcessor
|
||||
import chat.stoat.persistence.KVStorage
|
||||
import chat.stoat.screens.chat.ChatRouterDestination
|
||||
|
|
@ -75,7 +75,10 @@ class ChannelScreenViewModel @Inject constructor(
|
|||
var items = mutableStateListOf<ChannelScreenItem>()
|
||||
var typingUsers = mutableStateListOf<String>()
|
||||
|
||||
var channel by mutableStateOf<Channel?>(null)
|
||||
var channelId by mutableStateOf<String?>(null)
|
||||
val channel: Channel?
|
||||
get() = channelId?.let { StoatAPI.channelCache[it] }
|
||||
|
||||
var activePane by mutableStateOf<ChannelScreenActivePane>(ChannelScreenActivePane.None)
|
||||
var keyboardHeight by mutableIntStateOf(0)
|
||||
|
||||
|
|
@ -116,7 +119,7 @@ class ChannelScreenViewModel @Inject constructor(
|
|||
fun switchChannel(id: String) {
|
||||
// Reset state
|
||||
this.loadMessagesJob?.cancel()
|
||||
this.channel = StoatAPI.channelCache[id]
|
||||
this.channelId = id
|
||||
this.items = mutableStateListOf(ChannelScreenItem.Loading)
|
||||
this.activePane = ChannelScreenActivePane.None
|
||||
this.typingUsers = mutableStateListOf()
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ telephoto = "1.0.0-alpha02"
|
|||
haze = "1.7.2"
|
||||
chucker = "4.3.1"
|
||||
androidx-test = "1.6.1"
|
||||
livekit = "2.21.0"
|
||||
livekit-compose = "1.4.0"
|
||||
livekit = "2.24.1"
|
||||
livekit-compose = "2.3.0"
|
||||
|
||||
[libraries]
|
||||
android-core-ktx = { module = "androidx.core:core-ktx", version = "1.16.0" }
|
||||
|
|
@ -99,7 +99,7 @@ chucker = { module = "com.github.chuckerteam.chucker:library", version.ref = "ch
|
|||
chucker-noop = { module = "com.github.chuckerteam.chucker:library-no-op", version.ref = "chucker" }
|
||||
square-logcat = { module = "com.squareup.logcat:logcat", version = "0.1" }
|
||||
livekit-android = { module = "io.livekit:livekit-android", version.ref = "livekit" }
|
||||
livekit-android-camerax = { module = "io.livekit:android-camerax", version.ref = "livekit" }
|
||||
livekit-android-camerax = { module = "io.livekit:livekit-android-camerax", version.ref = "livekit" }
|
||||
livekit-android-compose = { module = "io.livekit:livekit-android-compose-components", version.ref = "livekit-compose" }
|
||||
|
||||
[plugins]
|
||||
|
|
|
|||
Loading…
Reference in New Issue