davidliu
Committed by GitHub

Handle new LeaveRequest protocol (#464)

... ... @@ -52,7 +52,7 @@ data class ConnectOptions(
/**
* the protocol version to use with the server.
*/
val protocolVersion: ProtocolVersion = ProtocolVersion.v12,
val protocolVersion: ProtocolVersion = ProtocolVersion.v13,
) {
internal var reconnect: Boolean = false
internal var participantSid: String? = null
... ...
... ... @@ -58,6 +58,7 @@ import livekit.LivekitModels
import livekit.LivekitModels.AudioTrackFeature
import livekit.LivekitRtc
import livekit.LivekitRtc.JoinResponse
import livekit.LivekitRtc.LeaveRequest
import livekit.LivekitRtc.ReconnectResponse
import livekit.org.webrtc.DataChannel
import livekit.org.webrtc.IceCandidate
... ... @@ -939,14 +940,34 @@ internal constructor(
listener?.onConnectionQuality(updates)
}
override fun onLeave(leave: LivekitRtc.LeaveRequest) {
if (leave.canReconnect) {
// reconnect will be triggered on close.
fullReconnectOnNext = true
} else {
close()
val disconnectReason = leave.reason.convert()
listener?.onEngineDisconnected(disconnectReason)
override fun onLeave(leave: LeaveRequest) {
LKLog.d { "leave request received: reason = ${leave.reason.name}" }
if (leave.hasRegions()) {
regionUrlProvider?.let {
it.setServerReportedRegions(RegionSettings.fromProto(leave.regions))
}
}
when {
leave.action == LeaveRequest.Action.RESUME -> {
// resume will be triggered on close.
// TODO: trigger immediately.
fullReconnectOnNext = false
}
leave.action == LeaveRequest.Action.RECONNECT ||
// canReconnect is deprecated protocol version >= 13
leave.canReconnect -> {
// resume will be triggered on close.
// TODO: trigger immediately.
fullReconnectOnNext = true
}
else -> {
close()
val disconnectReason = leave.reason.convert()
listener?.onEngineDisconnected(disconnectReason)
}
}
}
... ...
... ... @@ -28,6 +28,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import livekit.LivekitRtc
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.URI
... ... @@ -100,6 +101,11 @@ constructor(
attemptedRegions.clear()
}
fun setServerReportedRegions(regionSettings: RegionSettings) {
this.regionSettings = regionSettings
this.lastUpdateAt = SystemClock.elapsedRealtime()
}
@AssistedFactory
interface Factory {
fun create(serverUrl: URI, token: String): RegionUrlProvider
... ... @@ -136,7 +142,17 @@ fun setRegionUrlProviderTesting(enable: Boolean) {
* @suppress
*/
@Serializable
data class RegionSettings(val regions: List<RegionInfo>)
data class RegionSettings(val regions: List<RegionInfo>) {
companion object {
fun fromProto(proto: LivekitRtc.RegionSettings): RegionSettings {
return RegionSettings(
proto.regionsList.map { region ->
RegionInfo(region.region, region.url, region.distance)
},
)
}
}
}
/**
* @suppress
... ...
... ... @@ -113,6 +113,7 @@ constructor(
NODE_FAILURE,
MIGRATION,
SERVER_LEAVE,
SERVER_LEAVE_FULL_RECONNECT,
}
@Serializable
... ... @@ -878,6 +879,7 @@ constructor(
SimulateScenario.NODE_FAILURE -> builder.nodeFailure = true
SimulateScenario.MIGRATION -> builder.migration = true
SimulateScenario.SERVER_LEAVE -> builder.serverLeave = true
SimulateScenario.SERVER_LEAVE_FULL_RECONNECT -> builder.leaveRequestFullReconnect = true
}
sendSimulateScenario(builder.build())
}
... ...
... ... @@ -540,9 +540,16 @@ constructor(
}
fun sendLeave() {
val request = LivekitRtc.SignalRequest.newBuilder()
.setLeave(LivekitRtc.LeaveRequest.newBuilder().build())
.build()
val request = with(LivekitRtc.SignalRequest.newBuilder()) {
leave = with(LivekitRtc.LeaveRequest.newBuilder()) {
reason = LivekitModels.DisconnectReason.CLIENT_INITIATED
// server doesn't process this field, keeping it here to indicate the intent of a full disconnect
action = LivekitRtc.LeaveRequest.Action.DISCONNECT
build()
}
build()
}
sendRequest(request)
}
... ... @@ -906,4 +913,7 @@ enum class ProtocolVersion(val value: Int) {
v10(10),
v11(11),
v12(12),
// new leave request handling
v13(13),
}
... ...
... ... @@ -393,6 +393,10 @@ class CallViewModel(
room.sendSimulateScenario(Room.SimulateScenario.NODE_FAILURE)
}
fun simulateServerLeaveFullReconnect() {
room.sendSimulateScenario(Room.SimulateScenario.SERVER_LEAVE_FULL_RECONNECT)
}
fun reconnect() {
Timber.e { "Reconnecting." }
mutablePrimarySpeaker.value = null
... ...
... ... @@ -109,6 +109,7 @@ class CallActivity : AppCompatActivity() {
onSendMessage = { viewModel.sendData(it) },
onSimulateMigration = { viewModel.simulateMigration() },
onSimulateNodeFailure = { viewModel.simulateNodeFailure() },
onSimulateLeaveFullReconnect = { viewModel.simulateServerLeaveFullReconnect() },
fullReconnect = { viewModel.reconnect() },
)
}
... ... @@ -164,6 +165,7 @@ class CallActivity : AppCompatActivity() {
onSimulateMigration: () -> Unit = {},
onSimulateNodeFailure: () -> Unit = {},
fullReconnect: () -> Unit = {},
onSimulateLeaveFullReconnect: () -> Unit,
) {
AppTheme(darkTheme = true) {
ConstraintLayout(
... ... @@ -432,6 +434,7 @@ class CallActivity : AppCompatActivity() {
onDismissRequest = { showDebugDialog = false },
simulateMigration = { onSimulateMigration() },
simulateNodeFailure = { onSimulateNodeFailure() },
simulateLeaveFullReconnect = { onSimulateLeaveFullReconnect() },
fullReconnect = { fullReconnect() },
)
}
... ...
/*
* 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.composesample.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.Text
... ... @@ -20,6 +40,7 @@ fun DebugMenuDialog(
simulateMigration: () -> Unit = {},
fullReconnect: () -> Unit = {},
simulateNodeFailure: () -> Unit = {},
simulateLeaveFullReconnect: () -> Unit = {},
) {
Dialog(onDismissRequest = onDismissRequest) {
Column(
... ... @@ -44,6 +65,14 @@ fun DebugMenuDialog(
}) {
Text("Simulate Node Failure")
}
Button(
onClick = {
simulateLeaveFullReconnect()
onDismissRequest()
},
) {
Text("Simulate Server Leave Full Reconnect")
}
Button(onClick = {
fullReconnect()
onDismissRequest()
... ...