Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/shy-foxes-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": patch
---

Migrate from Klaxon decoding to kotlinx-serialization for AgentAttribute deserialization
3 changes: 0 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ groupie = "2.9.0"
junit-lib = "4.13.2"
junit-jupiter = "5.5.0"
jwtdecode = "2.0.2"
klaxon = "5.5"
kotlinx-serialization = "1.5.0"
leakcanaryAndroid = "2.8.1"
lint = "30.0.1"
Expand Down Expand Up @@ -51,8 +50,6 @@ dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref =
groupie = { module = "com.github.lisawray.groupie:groupie", version.ref = "groupie" }
groupie-viewbinding = { module = "com.github.lisawray.groupie:groupie-viewbinding", version.ref = "groupie" }
jwtdecode = { module = "com.auth0.android:jwtdecode", version.ref = "jwtdecode" }
klaxon = { module = "com.beust:klaxon", version.ref = "klaxon" }
noise = { module = "com.github.paramsen:noise", version.ref = "noise" }
androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" }
Expand Down
1 change: 0 additions & 1 deletion livekit-android-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ dependencies {
api libs.okhttp.lib
implementation libs.okhttp.coroutines
api libs.audioswitch
implementation libs.klaxon
implementation libs.jwtdecode

implementation libs.androidx.annotation
Expand Down
8 changes: 0 additions & 8 deletions livekit-android-sdk/consumer-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,3 @@
# Protobuf
#########################################
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }

# Klaxon JSON parsing
#########################################
# Klaxon uses reflection and doesn't ship ProGuard rules.
# Keep Klaxon library classes for reflection to work
-keep class com.beust.klaxon.** { *; }
-keep interface com.beust.klaxon.** { *; }
# Data classes using Klaxon should be annotated with @Keep at the source level
Original file line number Diff line number Diff line change
Expand Up @@ -16,61 +16,50 @@

package io.livekit.android.room.types

import android.annotation.SuppressLint
import androidx.annotation.Keep
import com.beust.klaxon.Converter
import com.beust.klaxon.Json
import com.beust.klaxon.JsonValue
import com.beust.klaxon.Klaxon
import io.livekit.android.util.LKLog
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

private fun <T> Klaxon.convert(k: kotlin.reflect.KClass<*>, fromJson: (JsonValue) -> T, toJson: (T) -> String, isUnion: Boolean = false) =
this.converter(
object : Converter {
@Suppress("UNCHECKED_CAST")
override fun toJson(value: Any) = toJson(value as T)
override fun fromJson(jv: JsonValue) = fromJson(jv) as Any?
override fun canConvert(cls: Class<*>) = cls == k.java || (isUnion && cls.superclass == k.java)
},
)

internal val klaxon = Klaxon()
.convert(AgentInput::class, { it.string?.let { AgentInput.fromValue(it) } }, { "\"${it?.value}\"" })
.convert(AgentOutput::class, { it.string?.let { AgentOutput.fromValue(it) } }, { "\"${it?.value}\"" })
.convert(AgentSdkState::class, { it.string?.let { AgentSdkState.fromValue(it) } }, { "\"${it?.value}\"" })
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Keep
@Serializable
data class AgentAttributes(
@Json(name = "lk.agent.inputs")
@SerialName("lk.agent.inputs")
val lkAgentInputs: List<AgentInput>? = null,

@Json(name = "lk.agent.outputs")
@SerialName("lk.agent.outputs")
val lkAgentOutputs: List<AgentOutput>? = null,

@Json(name = "lk.agent.state")
@SerialName("lk.agent.state")
val lkAgentState: AgentSdkState? = null,

@Json(name = "lk.publish_on_behalf")
@SerialName("lk.publish_on_behalf")
val lkPublishOnBehalf: String? = null,
) {
fun toJson() = klaxon.toJsonString(this)

companion object {
fun fromJson(json: String) = klaxon.parse<AgentAttributes>(json)
}
}
)

@Keep
@Serializable(with = AgentInputSerializer::class)
enum class AgentInput(val value: String) {
@SerialName("audio")
Audio("audio"),

@SerialName("text")
Text("text"),

@SerialName("video")
Video("video"),
Unknown("unknown"),
;

@SerialName("unknown")
Unknown("unknown");

companion object {
fun fromValue(value: String): AgentInput? = when (value) {
fun fromValue(value: String): AgentInput = when (value) {
"audio" -> Audio
"text" -> Text
"video" -> Video
Expand All @@ -83,14 +72,34 @@ enum class AgentInput(val value: String) {
}

@Keep
internal object AgentInputSerializer : KSerializer<AgentInput> {
// Serial names of descriptors should be unique, this is why we advise including app package in the name.
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.livekit.android.room.types.AgentInput", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: AgentInput) {
encoder.encodeString(value.value)
}

override fun deserialize(decoder: Decoder): AgentInput {
val string = decoder.decodeString()
return AgentInput.fromValue(string)
}
}

@Keep
@Serializable(with = AgentOutputSerializer::class)
enum class AgentOutput(val value: String) {
@SerialName("audio")
Audio("audio"),

@SerialName("transcription")
Transcription("transcription"),
Unknown("unknown"),
;

@SerialName("unknown")
Unknown("unknown");

companion object {
fun fromValue(value: String): AgentOutput? = when (value) {
fun fromValue(value: String): AgentOutput = when (value) {
"audio" -> Audio
"transcription" -> Transcription
else -> {
Expand All @@ -101,19 +110,45 @@ enum class AgentOutput(val value: String) {
}
}

@Keep
internal object AgentOutputSerializer : KSerializer<AgentOutput> {
// Serial names of descriptors should be unique, this is why we advise including app package in the name.
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.livekit.android.room.types.AgentOutput", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: AgentOutput) {
encoder.encodeString(value.value)
}

override fun deserialize(decoder: Decoder): AgentOutput {
val string = decoder.decodeString()
return AgentOutput.fromValue(string)
}
}

// Renamed from AgentState to AgentSdkState to avoid naming conflicts elsewhere.
@Keep
@Serializable(with = AgentSdkStateSerializer::class)
enum class AgentSdkState(val value: String) {
@SerialName("idle")
Idle("idle"),

@SerialName("initializing")
Initializing("initializing"),

@SerialName("listening")
Listening("listening"),

@SerialName("speaking")
Speaking("speaking"),

@SerialName("thinking")
Thinking("thinking"),
Unknown("unknown"),
;

@SerialName("unknown")
Unknown("unknown");

companion object {
fun fromValue(value: String): AgentSdkState? = when (value) {
fun fromValue(value: String): AgentSdkState = when (value) {
"idle" -> Idle
"initializing" -> Initializing
"listening" -> Listening
Expand All @@ -127,34 +162,42 @@ enum class AgentSdkState(val value: String) {
}
}

@Keep
internal object AgentSdkStateSerializer : KSerializer<AgentSdkState> {
// Serial names of descriptors should be unique, this is why we advise including app package in the name.
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.livekit.android.room.types.AgentSdkState", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: AgentSdkState) {
encoder.encodeString(value.value)
}

override fun deserialize(decoder: Decoder): AgentSdkState {
val string = decoder.decodeString()
return AgentSdkState.fromValue(string)
}
}

/**
* Schema for transcription-related attributes
*/
@Keep
@SuppressLint("UnsafeOptInUsageError")
@Serializable
data class TranscriptionAttributes(
/**
* The segment id of the transcription
*/
@Json(name = "lk.segment_id")
@SerialName("lk.segment_id")
val lkSegmentID: String? = null,

/**
* The associated track id of the transcription
*/
@Json(name = "lk.transcribed_track_id")
@SerialName("lk.transcribed_track_id")
val lkTranscribedTrackID: String? = null,

/**
* Whether the transcription is final
*/
@Json(name = "lk.transcription_final")
@SerialName("lk.transcription_final")
val lkTranscriptionFinal: Boolean? = null,
) {
fun toJson() = klaxon.toJsonString(this)

companion object {
fun fromJson(json: String) = klaxon.parse<TranscriptionAttributes>(json)
}
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,44 @@

package io.livekit.android.room.types

import com.beust.klaxon.JsonObject
import androidx.annotation.VisibleForTesting
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromJsonElement

// AgentTypes.kt is a generated file and should not be edited.
// Add any required functions through extensions here.

private val jsonSerializer = Json {
allowStructuredMapKeys = true
coerceInputValues = true
}
internal fun AgentAttributes.Companion.fromJsonObject(jsonObject: JsonObject) =
klaxon.parseFromJsonObject<AgentAttributes>(jsonObject)
jsonSerializer.decodeFromJsonElement<AgentAttributes>(jsonObject)

/**
* @suppress
*/
fun AgentAttributes.Companion.fromMap(map: Map<String, *>): AgentAttributes {
if (map.values.all { it == null }) {
fun AgentAttributes.Companion.fromMap(map: Map<String, JsonElement>): AgentAttributes {
if (map.values.none()) {
return AgentAttributes()
}

return fromJsonObject(JsonObject(map)) ?: AgentAttributes()
return fromJsonObject(JsonObject(map))
}

/**
* @suppress
*/
fun AgentAttributes.Companion.fromStringMap(map: Map<String, String>): AgentAttributes {
val parseMap = mutableMapOf<String, Any?>()
val parseMap = mutableMapOf<String, JsonElement>()
for ((key, converter) in AGENT_ATTRIBUTES_CONVERSION) {
parseMap[key] = converter(map[key])
converter(map[key])?.let { converted ->
parseMap[key] = converted
}
}

return fromMap(parseMap)
Expand All @@ -51,40 +63,49 @@ fun AgentAttributes.Companion.fromStringMap(map: Map<String, String>): AgentAttr
* Protobuf attribute maps are [String, String], so need to parse arrays/maps manually.
* @suppress
*/
val AGENT_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> Any?>(
"lk.agent.inputs" to { json -> json?.let { klaxon.parseArray<List<String>>(json) } },
"lk.agent.outputs" to { json -> json?.let { klaxon.parseArray<List<String>>(json) } },
"lk.agent.state" to { json -> json },
"lk.publish_on_behalf" to { json -> json },
@VisibleForTesting
val AGENT_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> JsonElement?>(
"lk.agent.inputs" to { json -> json?.let { jsonSerializer.decodeFromString<JsonArray>(json) } },
"lk.agent.outputs" to { json -> json?.let { jsonSerializer.decodeFromString<JsonArray>(json) } },
"lk.agent.state" to { json -> JsonPrimitive(json) },
"lk.publish_on_behalf" to { json -> JsonPrimitive(json) },
)

internal fun TranscriptionAttributes.Companion.fromJsonObject(jsonObject: JsonObject) =
klaxon.parseFromJsonObject<TranscriptionAttributes>(jsonObject)
jsonSerializer.decodeFromJsonElement<TranscriptionAttributes>(jsonObject)

/**
* @suppress
*/
fun TranscriptionAttributes.Companion.fromMap(map: Map<String, *>): TranscriptionAttributes {
return fromJsonObject(JsonObject(map)) ?: TranscriptionAttributes()
fun TranscriptionAttributes.Companion.fromMap(map: Map<String, JsonElement>): TranscriptionAttributes {
if (map.values.none()) {
return TranscriptionAttributes()
}

return fromJsonObject(JsonObject(map))
}

/**
* @suppress
*/
fun TranscriptionAttributes.Companion.fromStringMap(map: Map<String, String>): TranscriptionAttributes {
val parseMap = mutableMapOf<String, Any?>()
val parseMap = mutableMapOf<String, JsonElement>()
for ((key, converter) in TRANSCRIPTION_ATTRIBUTES_CONVERSION) {
parseMap[key] = converter(map[key])
converter(map[key])?.let { converted ->
parseMap[key] = converted
}
}

return fromMap(parseMap)
}

/**
* Protobuf attribute maps are [String, String], so need to parse arrays/maps manually.
* @suppress
*/
val TRANSCRIPTION_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> Any?>(
"lk.segment_id" to { json -> json },
"lk.transcribed_track_id" to { json -> json },
"lk.transcription_final" to { json -> json?.let { klaxon.parse(json) } },
@VisibleForTesting
val TRANSCRIPTION_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> JsonElement?>(
"lk.segment_id" to { json -> JsonPrimitive(json) },
"lk.transcribed_track_id" to { json -> JsonPrimitive(json) },
"lk.transcription_final" to { json -> json?.let { jsonSerializer.decodeFromString<JsonArray>(json) } },
)
2 changes: 1 addition & 1 deletion livekit-android-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ dependencies {
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.okhttp.mockwebserver
testImplementation libs.klaxon
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
kaptTest libs.dagger.compiler

androidTestImplementation libs.androidx.test.junit
Expand Down
Loading