From 73901797a8e4d7f2a193dfd358ffc42bd3dcfe52 Mon Sep 17 00:00:00 2001 From: davidliu Date: Mon, 8 Jun 2026 21:47:37 +0900 Subject: [PATCH 1/2] Add support for generic request response --- .changeset/late-lamps-greet.md | 5 + .changeset/polite-goats-refuse.md | 15 ++ .../java/io/livekit/android/ConnectOptions.kt | 2 +- .../java/io/livekit/android/room/RTCEngine.kt | 5 + .../main/java/io/livekit/android/room/Room.kt | 7 + .../io/livekit/android/room/SignalClient.kt | 19 +- .../room/participant/LocalParticipant.kt | 148 ++++++++++++++ .../android/room/signal/SignalRequest.kt | 72 +++++++ .../android/proto/ProtoConverterTest.kt | 5 + .../LocalParticipantRequestResponseTest.kt | 189 ++++++++++++++++++ 10 files changed, 464 insertions(+), 3 deletions(-) create mode 100644 .changeset/late-lamps-greet.md create mode 100644 .changeset/polite-goats-refuse.md create mode 100644 livekit-android-sdk/src/main/java/io/livekit/android/room/signal/SignalRequest.kt create mode 100644 livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantRequestResponseTest.kt diff --git a/.changeset/late-lamps-greet.md b/.changeset/late-lamps-greet.md new file mode 100644 index 000000000..67ce31650 --- /dev/null +++ b/.changeset/late-lamps-greet.md @@ -0,0 +1,5 @@ +--- +"client-sdk-android": minor +--- + +Add support for generic RequestResponse diff --git a/.changeset/polite-goats-refuse.md b/.changeset/polite-goats-refuse.md new file mode 100644 index 000000000..5dc93133e --- /dev/null +++ b/.changeset/polite-goats-refuse.md @@ -0,0 +1,15 @@ +--- +"client-sdk-android": minor +--- + +Add the following suspend methods to LocalParticipant: + +- `setName` +- `setMetadata` +- `setAttributes` + +These replace the following deprecated methods: + +- `updateName` +- `updateMetadata` +- `updateAttributes` diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/ConnectOptions.kt b/livekit-android-sdk/src/main/java/io/livekit/android/ConnectOptions.kt index b7fe11b86..b90d797d6 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/ConnectOptions.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/ConnectOptions.kt @@ -53,7 +53,7 @@ data class ConnectOptions( /** * the protocol version to use with the server. */ - val protocolVersion: ProtocolVersion = ProtocolVersion.v13, + val protocolVersion: ProtocolVersion = ProtocolVersion.v15, /** * The client protocol version to advertise to other participants in the room diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/RTCEngine.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/RTCEngine.kt index dee0adab0..05f59992c 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/room/RTCEngine.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/RTCEngine.kt @@ -1037,6 +1037,7 @@ internal constructor( fun onLocalTrackUnpublished(trackUnpublished: LivekitRtc.TrackUnpublishedResponse) fun onTranscriptionReceived(transcription: LivekitModels.Transcription) fun onLocalTrackSubscribed(trackSubscribed: LivekitRtc.TrackSubscribed) + fun onRequestResponse(response: LivekitRtc.RequestResponse) fun onRpcPacketReceived(dp: LivekitModels.DataPacket) fun onDataStreamPacket(dp: LivekitModels.DataPacket, encryptionType: LivekitModels.Encryption.Type) } @@ -1280,6 +1281,10 @@ internal constructor( listener?.onLocalTrackUnpublished(trackUnpublished) } + override fun onRequestResponse(response: LivekitRtc.RequestResponse) { + listener?.onRequestResponse(response) + } + // --------------------------------- DataChannel.Observer ------------------------------------// fun onBufferedAmountChange(dataChannel: DataChannel, previousAmount: Long) { diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt index 9d5c71bf3..afc4676b0 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt @@ -1422,6 +1422,13 @@ constructor( localParticipant.handleSubscribedQualityUpdate(subscribedQualityUpdate) } + /** + * @suppress + */ + override fun onRequestResponse(response: LivekitRtc.RequestResponse) { + localParticipant.handleRequestResponse(response) + } + /** * @suppress */ diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/SignalClient.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/SignalClient.kt index 5951becc6..5b7276f77 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/room/SignalClient.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/SignalClient.kt @@ -60,6 +60,7 @@ import okhttp3.WebSocketListener import okio.ByteString import okio.ByteString.Companion.toByteString import java.util.Date +import java.util.concurrent.atomic.AtomicInteger import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton @@ -97,6 +98,7 @@ constructor( private var lastUrl: String? = null internal var lastOptions: ConnectOptions? = null private var lastRoomOptions: RoomOptions? = null + private val nextRequestId = AtomicInteger(0) // join will always return a JoinResponse. // reconnect will return a ReconnectResponse or a Unit if a different response was received. @@ -563,8 +565,16 @@ constructor( sendRequest(request) } - fun sendUpdateLocalMetadata(metadata: String?, name: String?, attributes: Map? = emptyMap()) { + internal fun allocateRequestId(): Int = nextRequestId.incrementAndGet() + + fun sendUpdateLocalMetadata( + metadata: String?, + name: String?, + attributes: Map = emptyMap(), + requestId: Int = allocateRequestId(), + ): Int { val update = LivekitRtc.UpdateParticipantMetadata.newBuilder() + .setRequestId(requestId) .setMetadata(metadata ?: "") .setName(name ?: "") .putAllAttributes(attributes) @@ -574,6 +584,7 @@ constructor( .build() sendRequest(request) + return requestId } fun sendSyncState(syncState: LivekitRtc.SyncState) { @@ -837,7 +848,7 @@ constructor( } LivekitRtc.SignalResponse.MessageCase.REQUEST_RESPONSE -> { - // TODO + listener?.onRequestResponse(response.requestResponse) } LivekitRtc.SignalResponse.MessageCase.ROOM_MOVED -> { @@ -958,6 +969,7 @@ constructor( fun onRefreshToken(token: String) fun onLocalTrackUnpublished(trackUnpublished: LivekitRtc.TrackUnpublishedResponse) fun onLocalTrackSubscribed(trackSubscribed: LivekitRtc.TrackSubscribed) + fun onRequestResponse(response: LivekitRtc.RequestResponse) } companion object { @@ -1025,6 +1037,9 @@ enum class ProtocolVersion(val value: Int) { // new leave request handling v13(13), + + // signal request response handling + v15(15), } /** diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt index 5daa840db..52a4620ac 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt @@ -41,6 +41,7 @@ import io.livekit.android.room.isSVCCodec import io.livekit.android.room.rpc.RpcClientManager import io.livekit.android.room.rpc.RpcManager import io.livekit.android.room.rpc.RpcServerManager +import io.livekit.android.room.signal.SignalRequestException import io.livekit.android.room.track.DataPublishReliability import io.livekit.android.room.track.LocalAudioTrack import io.livekit.android.room.track.LocalAudioTrackOptions @@ -59,13 +60,19 @@ import io.livekit.android.room.track.screencapture.ScreenCaptureParams import io.livekit.android.room.util.EncodingUtils import io.livekit.android.rpc.RpcError import io.livekit.android.util.LKLog +import io.livekit.android.util.TimeoutException import io.livekit.android.util.flow import io.livekit.android.util.rethrowIfCancellationSignal +import io.livekit.android.util.withDeadline import io.livekit.android.webrtc.sortVideoCodecPreferences +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -90,6 +97,7 @@ import javax.inject.Named import kotlin.math.max import kotlin.math.min import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds class LocalParticipant @AssistedInject @@ -130,6 +138,8 @@ internal constructor( private val jobs = mutableMapOf() + private val pendingSignalRequests = Collections.synchronizedMap(mutableMapOf>()) + // For ensuring that only one caller can execute setTrackEnabled at a time. // Without it, there's a potential to create multiple of the same source, // Camera has deadlock issues with multiple CameraCapturers trying to activate/stop. @@ -1109,12 +1119,59 @@ internal constructor( } } + /** + * Sets and updates the metadata of the local participant. + * Note: this requires `CanUpdateOwnMetadata` permission encoded in the token. + * + * @param metadata the metadata to set + * @return a [Result] that succeeds when the server confirms the update, or fails with + * [SignalRequestException] if the server rejects the request or + * [TimeoutException] if it times out + */ + @CheckResult + suspend fun setMetadata(metadata: String): Result { + return requestMetadataUpdate(metadata = metadata) + } + + /** + * Sets and updates the name of the local participant. + * Note: this requires `CanUpdateOwnMetadata` permission encoded in the token. + * + * @param name the name to set + * @return a [Result] that succeeds when the server confirms the update, or fails with + * [SignalRequestException] if the server rejects the request or + * [TimeoutException] if it times out + */ + @CheckResult + suspend fun setName(name: String): Result { + return requestMetadataUpdate(name = name) + } + + /** + * Set or update participant attributes. It will make updates only to keys that + * are present in [attributes], and will not override others. + * + * To delete a value, set the value to an empty string. + * + * Note: this requires `CanUpdateOwnMetadata` permission encoded in the token. + * + * @param attributes attributes to update + * @return a [Result] that succeeds when the server confirms the update, or fails with + * [SignalRequestException] if the server rejects the request or + * [TimeoutException] if it times out + */ + @CheckResult + suspend fun setAttributes(attributes: Map): Result { + return requestMetadataUpdate(attributes = attributes) + } + /** * Updates the metadata of the local participant. Changes will not be reflected until the * server responds confirming the update. * Note: this requires `CanUpdateOwnMetadata` permission encoded in the token. * @param metadata */ + @Deprecated(message = "Use the suspend function setMetadata instead.") fun updateMetadata(metadata: String) { this.engine.client.sendUpdateLocalMetadata(metadata, name) } @@ -1125,6 +1182,7 @@ internal constructor( * Note: this requires `CanUpdateOwnMetadata` permission encoded in the token. * @param name */ + @Deprecated(message = "Use the suspend function setName instead.") fun updateName(name: String) { this.engine.client.sendUpdateLocalMetadata(metadata, name) } @@ -1138,6 +1196,7 @@ internal constructor( * Note: this requires `canUpdateOwnMetadata` permission. * @param attributes attributes to update */ + @Deprecated(message = "Use the suspend function setAttributes instead.") fun updateAttributes(attributes: Map) { this.engine.client.sendUpdateLocalMetadata(metadata, name, attributes) } @@ -1147,6 +1206,88 @@ internal constructor( pub?.muted = muted } + internal fun handleRequestResponse(response: LivekitRtc.RequestResponse) { + val deferred = pendingSignalRequests[response.requestId] ?: return + if (response.reason != LivekitRtc.RequestResponse.Reason.OK) { + pendingSignalRequests.remove(response.requestId) + deferred.completeExceptionally(SignalRequestException.fromResponse(response)) + return + } + pendingSignalRequests.remove(response.requestId) + } + + private suspend fun requestMetadataUpdate( + metadata: String? = null, + name: String? = null, + attributes: Map? = null, + ): Result { + val requestId = engine.client.allocateRequestId() + val deferred = CompletableDeferred() + pendingSignalRequests[requestId] = deferred + + return try { + engine.client.sendUpdateLocalMetadata( + metadata = metadata ?: this.metadata, + name = name ?: this.name, + attributes = attributes ?: emptyMap(), + requestId = requestId, + ) + withDeadline(METADATA_UPDATE_TIMEOUT) { + coroutineScope { + val confirmationJob = launch { + combine( + ::name.flow, + ::metadata.flow, + ::attributes.flow, + ) { _, _, _ -> } + .first { isMetadataUpdateConfirmed(metadata, name, attributes) } + if (!deferred.isCompleted) { + deferred.complete(Unit) + } + } + try { + deferred.await() + } finally { + confirmationJob.cancel() + } + } + } + Result.success(Unit) + } catch (e: TimeoutException) { + deferred.completeExceptionally(e) + Result.failure(e) + } catch (e: CancellationException) { + deferred.cancel() + throw e + } catch (e: Exception) { + Result.failure(e) + } finally { + pendingSignalRequests.remove(requestId) + } + } + + private fun isMetadataUpdateConfirmed( + metadata: String?, + name: String?, + attributes: Map?, + ): Boolean { + if (name != null && this.name != name) { + return false + } + if (metadata != null && this.metadata != metadata) { + return false + } + if (attributes != null && + !attributes.all { (key, value) -> + val current = this.attributes[key] + current == value || (value.isEmpty() && current.isNullOrEmpty()) + } + ) { + return false + } + return true + } + internal fun handleSubscribedQualityUpdate(subscribedQualityUpdate: LivekitRtc.SubscribedQualityUpdate) { if (!dynacast) { return @@ -1319,6 +1460,9 @@ internal constructor( * @suppress */ fun cleanup() { + pendingSignalRequests.values.forEach { it.cancel() } + pendingSignalRequests.clear() + for (pub in trackPublications.values) { val track = pub.track @@ -1394,6 +1538,10 @@ internal constructor( interface Factory { fun create(dynacast: Boolean): LocalParticipant } + + companion object { + private val METADATA_UPDATE_TIMEOUT = 5_000.milliseconds + } } internal fun LocalParticipant.publishTracksInfo(): List { diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/signal/SignalRequest.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/signal/SignalRequest.kt new file mode 100644 index 000000000..99d304042 --- /dev/null +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/signal/SignalRequest.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2026 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.room.signal + +import livekit.LivekitRtc + +/** + * Error returned when a signal request is rejected by the server + */ +class SignalRequestException( + message: String? = null, + val reason: SignalResponseReason, + cause: Throwable? = null, +) : Exception(message, cause) { + + companion object { + fun fromResponse(response: LivekitRtc.RequestResponse): SignalRequestException { + return SignalRequestException( + message = response.message.takeIf { it.isNotEmpty() }, + reason = SignalResponseReason.fromProto(response.reason), + ) + } + } +} + +enum class SignalResponseReason { + OK, + NOT_FOUND, + NOT_ALLOWED, + LIMIT_EXCEEDED, + QUEUED, + UNSUPPORTED_TYPE, + UNCLASSIFIED_ERROR, + INVALID_HANDLE, + INVALID_NAME, + DUPLICATE_HANDLE, + DUPLICATE_NAME, + UNRECOGNIZED; + + companion object { + internal fun fromProto(proto: LivekitRtc.RequestResponse.Reason): SignalResponseReason { + return when (proto) { + LivekitRtc.RequestResponse.Reason.OK -> OK + LivekitRtc.RequestResponse.Reason.NOT_FOUND -> NOT_FOUND + LivekitRtc.RequestResponse.Reason.NOT_ALLOWED -> NOT_ALLOWED + LivekitRtc.RequestResponse.Reason.LIMIT_EXCEEDED -> LIMIT_EXCEEDED + LivekitRtc.RequestResponse.Reason.QUEUED -> QUEUED + LivekitRtc.RequestResponse.Reason.UNSUPPORTED_TYPE -> UNSUPPORTED_TYPE + LivekitRtc.RequestResponse.Reason.UNCLASSIFIED_ERROR -> UNCLASSIFIED_ERROR + LivekitRtc.RequestResponse.Reason.INVALID_HANDLE -> INVALID_HANDLE + LivekitRtc.RequestResponse.Reason.INVALID_NAME -> INVALID_NAME + LivekitRtc.RequestResponse.Reason.DUPLICATE_HANDLE -> DUPLICATE_HANDLE + LivekitRtc.RequestResponse.Reason.DUPLICATE_NAME -> DUPLICATE_NAME + else -> UNRECOGNIZED + } + } + } +} diff --git a/livekit-android-test/src/test/java/io/livekit/android/proto/ProtoConverterTest.kt b/livekit-android-test/src/test/java/io/livekit/android/proto/ProtoConverterTest.kt index 8bd9a933f..ca6b844e3 100644 --- a/livekit-android-test/src/test/java/io/livekit/android/proto/ProtoConverterTest.kt +++ b/livekit-android-test/src/test/java/io/livekit/android/proto/ProtoConverterTest.kt @@ -18,6 +18,7 @@ package io.livekit.android.proto import io.livekit.android.room.RegionSettings import io.livekit.android.room.participant.ParticipantPermission +import io.livekit.android.room.signal.SignalResponseReason import io.livekit.android.rpc.RpcError import io.livekit.android.token.RoomAgentDispatch import io.livekit.android.token.RoomConfiguration @@ -100,6 +101,10 @@ class ProtoConverterTest( RoomAgentDispatch::class.java, whitelist = listOf("restartPolicy", "deployment"), ), + ProtoConverterTestCase( + LivekitRtc.RequestResponse.Reason::class.java, + SignalResponseReason::class.java, + ), ) @JvmStatic diff --git a/livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantRequestResponseTest.kt b/livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantRequestResponseTest.kt new file mode 100644 index 000000000..212096813 --- /dev/null +++ b/livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantRequestResponseTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2026 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.room.participant + +import io.livekit.android.room.signal.SignalRequestException +import io.livekit.android.room.signal.SignalResponseReason +import io.livekit.android.test.MockE2ETest +import io.livekit.android.test.mock.TestData +import io.livekit.android.test.util.toPBByteString +import io.livekit.android.util.TimeoutException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import livekit.LivekitModels +import livekit.LivekitRtc +import livekit.LivekitRtc.ParticipantUpdate +import livekit.LivekitRtc.RequestResponse +import livekit.LivekitRtc.SignalResponse +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) +class LocalParticipantRequestResponseTest : MockE2ETest() { + + @Test + fun setMetadataSendsRequestId() = runTest { + connect() + registerMetadataConfirmationHandler() + wsFactory.ws.clearRequests() + + val result = room.localParticipant.setMetadata("new_metadata") + + assertTrue(result.isSuccess) + val sentRequest = parseSentUpdateMetadataRequest() + assertTrue(sentRequest.updateMetadata.requestId > 0) + assertEquals("new_metadata", sentRequest.updateMetadata.metadata) + } + + @Test + fun setMetadataSucceedsOnParticipantUpdate() = runTest { + connect() + registerMetadataConfirmationHandler() + + val newMetadata = "confirmed_metadata" + val result = room.localParticipant.setMetadata(newMetadata) + + assertTrue(result.isSuccess) + assertEquals(newMetadata, room.localParticipant.metadata) + } + + @Test + fun setNameSucceedsOnParticipantUpdate() = runTest { + connect() + registerMetadataConfirmationHandler() + + val newName = "confirmed_name" + val result = room.localParticipant.setName(newName) + + assertTrue(result.isSuccess) + assertEquals(newName, room.localParticipant.name) + } + + @Test + fun setAttributesSucceedsOnParticipantUpdate() = runTest { + connect() + registerMetadataConfirmationHandler() + + val newAttributes = mapOf("attribute" to "changedValue") + val result = room.localParticipant.setAttributes(newAttributes) + + assertTrue(result.isSuccess) + assertEquals("changedValue", room.localParticipant.attributes["attribute"]) + } + + @Test + fun setMetadataFailsOnRequestResponseError() = runTest { + connect() + wsFactory.registerSignalRequestHandler { request -> + if (request.hasUpdateMetadata()) { + wsFactory.receiveMessage( + requestResponse( + requestId = request.updateMetadata.requestId, + reason = RequestResponse.Reason.NOT_ALLOWED, + message = "not allowed", + ), + ) + return@registerSignalRequestHandler true + } + return@registerSignalRequestHandler false + } + + val result = room.localParticipant.setMetadata("new_metadata") + + assertTrue(result.isFailure) + val error = result.exceptionOrNull() + assertTrue(error is SignalRequestException) + error as SignalRequestException + assertEquals(SignalResponseReason.NOT_ALLOWED, error.reason) + assertEquals("not allowed", error.message) + } + + @Test + fun setMetadataTimesOutWithoutConfirmation() = runTest { + connect() + wsFactory.ws.clearRequests() + + val deferred = async { + room.localParticipant.setMetadata("new_metadata") + } + coroutineRule.dispatcher.scheduler.advanceTimeBy(5_001) + + val result = deferred.await() + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is TimeoutException) + } + + private fun registerMetadataConfirmationHandler() { + wsFactory.registerSignalRequestHandler { request -> + if (request.hasUpdateMetadata()) { + val update = request.updateMetadata + val newInfo = with(TestData.LOCAL_PARTICIPANT.toBuilder()) { + if (update.metadata.isNotEmpty()) { + metadata = update.metadata + } + if (update.name.isNotEmpty()) { + name = update.name + } + if (update.attributesCount > 0) { + putAllAttributes(update.attributesMap) + } + build() + } + wsFactory.receiveMessage(participantUpdate(newInfo)) + return@registerSignalRequestHandler true + } + return@registerSignalRequestHandler false + } + } + + private fun parseSentUpdateMetadataRequest(): LivekitRtc.SignalRequest { + val requestString = wsFactory.ws.sentRequests.first().toPBByteString() + return LivekitRtc.SignalRequest.newBuilder() + .mergeFrom(requestString) + .build() + } + + private fun participantUpdate(participant: LivekitModels.ParticipantInfo): SignalResponse { + return SignalResponse.newBuilder() + .setUpdate( + ParticipantUpdate.newBuilder() + .addParticipants(participant) + .build(), + ) + .build() + } + + private fun requestResponse( + requestId: Int, + reason: RequestResponse.Reason, + message: String = "", + ): SignalResponse { + return SignalResponse.newBuilder() + .setRequestResponse( + RequestResponse.newBuilder() + .setRequestId(requestId) + .setReason(reason) + .setMessage(message) + .build(), + ) + .build() + } +} From 52f476584c3757e5d31f63f2b245df9965d8789c Mon Sep 17 00:00:00 2001 From: davidliu Date: Mon, 8 Jun 2026 22:09:14 +0900 Subject: [PATCH 2/2] Add complex condition to baseline --- livekit-android-sdk/detekt-baseline-release.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit-android-sdk/detekt-baseline-release.xml b/livekit-android-sdk/detekt-baseline-release.xml index 418e8ea50..799319cca 100644 --- a/livekit-android-sdk/detekt-baseline-release.xml +++ b/livekit-android-sdk/detekt-baseline-release.xml @@ -3,6 +3,7 @@ ComplexCondition:LocalParticipant.kt$LocalParticipant$(originalEncoding == null && !simulcast) || width == 0 || height == 0 + ComplexCondition:LocalParticipant.kt$LocalParticipant$attributes != null && !attributes.all { (key, value) -> val current = this.attributes[key] current == value || (value.isEmpty() && current.isNullOrEmpty()) } ComplexCondition:PeerConnectionTransport.kt$PeerConnectionTransport$sd.type == SessionDescription.Type.ANSWER && currentOfferId > 0 && offerId > 0 && currentOfferId > offerId ComplexCondition:PreconnectAudioBuffer.kt$sentIdentities.contains(identity) || kind != Participant.Kind.AGENT || state != Participant.State.ACTIVE || identity == null ComplexCondition:RTCEngine.kt$RTCEngine$(connectionState == ConnectionState.CONNECTED || connectionState == ConnectionState.RESUMING) && subscriberConnected && publisherConnected