davidliu
Committed by GitHub

Handle new LeaveRequest protocol (#464)

@@ -52,7 +52,7 @@ data class ConnectOptions( @@ -52,7 +52,7 @@ data class ConnectOptions(
52 /** 52 /**
53 * the protocol version to use with the server. 53 * the protocol version to use with the server.
54 */ 54 */
55 - val protocolVersion: ProtocolVersion = ProtocolVersion.v12, 55 + val protocolVersion: ProtocolVersion = ProtocolVersion.v13,
56 ) { 56 ) {
57 internal var reconnect: Boolean = false 57 internal var reconnect: Boolean = false
58 internal var participantSid: String? = null 58 internal var participantSid: String? = null
@@ -58,6 +58,7 @@ import livekit.LivekitModels @@ -58,6 +58,7 @@ import livekit.LivekitModels
58 import livekit.LivekitModels.AudioTrackFeature 58 import livekit.LivekitModels.AudioTrackFeature
59 import livekit.LivekitRtc 59 import livekit.LivekitRtc
60 import livekit.LivekitRtc.JoinResponse 60 import livekit.LivekitRtc.JoinResponse
  61 +import livekit.LivekitRtc.LeaveRequest
61 import livekit.LivekitRtc.ReconnectResponse 62 import livekit.LivekitRtc.ReconnectResponse
62 import livekit.org.webrtc.DataChannel 63 import livekit.org.webrtc.DataChannel
63 import livekit.org.webrtc.IceCandidate 64 import livekit.org.webrtc.IceCandidate
@@ -939,14 +940,34 @@ internal constructor( @@ -939,14 +940,34 @@ internal constructor(
939 listener?.onConnectionQuality(updates) 940 listener?.onConnectionQuality(updates)
940 } 941 }
941 942
942 - override fun onLeave(leave: LivekitRtc.LeaveRequest) {  
943 - if (leave.canReconnect) {  
944 - // reconnect will be triggered on close.  
945 - fullReconnectOnNext = true  
946 - } else {  
947 - close()  
948 - val disconnectReason = leave.reason.convert()  
949 - listener?.onEngineDisconnected(disconnectReason) 943 + override fun onLeave(leave: LeaveRequest) {
  944 + LKLog.d { "leave request received: reason = ${leave.reason.name}" }
  945 + if (leave.hasRegions()) {
  946 + regionUrlProvider?.let {
  947 + it.setServerReportedRegions(RegionSettings.fromProto(leave.regions))
  948 + }
  949 + }
  950 +
  951 + when {
  952 + leave.action == LeaveRequest.Action.RESUME -> {
  953 + // resume will be triggered on close.
  954 + // TODO: trigger immediately.
  955 + fullReconnectOnNext = false
  956 + }
  957 +
  958 + leave.action == LeaveRequest.Action.RECONNECT ||
  959 + // canReconnect is deprecated protocol version >= 13
  960 + leave.canReconnect -> {
  961 + // resume will be triggered on close.
  962 + // TODO: trigger immediately.
  963 + fullReconnectOnNext = true
  964 + }
  965 +
  966 + else -> {
  967 + close()
  968 + val disconnectReason = leave.reason.convert()
  969 + listener?.onEngineDisconnected(disconnectReason)
  970 + }
950 } 971 }
951 } 972 }
952 973
@@ -28,6 +28,7 @@ import kotlinx.coroutines.coroutineScope @@ -28,6 +28,7 @@ import kotlinx.coroutines.coroutineScope
28 import kotlinx.serialization.Serializable 28 import kotlinx.serialization.Serializable
29 import kotlinx.serialization.decodeFromString 29 import kotlinx.serialization.decodeFromString
30 import kotlinx.serialization.json.Json 30 import kotlinx.serialization.json.Json
  31 +import livekit.LivekitRtc
31 import okhttp3.OkHttpClient 32 import okhttp3.OkHttpClient
32 import okhttp3.Request 33 import okhttp3.Request
33 import java.net.URI 34 import java.net.URI
@@ -100,6 +101,11 @@ constructor( @@ -100,6 +101,11 @@ constructor(
100 attemptedRegions.clear() 101 attemptedRegions.clear()
101 } 102 }
102 103
  104 + fun setServerReportedRegions(regionSettings: RegionSettings) {
  105 + this.regionSettings = regionSettings
  106 + this.lastUpdateAt = SystemClock.elapsedRealtime()
  107 + }
  108 +
103 @AssistedFactory 109 @AssistedFactory
104 interface Factory { 110 interface Factory {
105 fun create(serverUrl: URI, token: String): RegionUrlProvider 111 fun create(serverUrl: URI, token: String): RegionUrlProvider
@@ -136,7 +142,17 @@ fun setRegionUrlProviderTesting(enable: Boolean) { @@ -136,7 +142,17 @@ fun setRegionUrlProviderTesting(enable: Boolean) {
136 * @suppress 142 * @suppress
137 */ 143 */
138 @Serializable 144 @Serializable
139 -data class RegionSettings(val regions: List<RegionInfo>) 145 +data class RegionSettings(val regions: List<RegionInfo>) {
  146 + companion object {
  147 + fun fromProto(proto: LivekitRtc.RegionSettings): RegionSettings {
  148 + return RegionSettings(
  149 + proto.regionsList.map { region ->
  150 + RegionInfo(region.region, region.url, region.distance)
  151 + },
  152 + )
  153 + }
  154 + }
  155 +}
140 156
141 /** 157 /**
142 * @suppress 158 * @suppress
@@ -113,6 +113,7 @@ constructor( @@ -113,6 +113,7 @@ constructor(
113 NODE_FAILURE, 113 NODE_FAILURE,
114 MIGRATION, 114 MIGRATION,
115 SERVER_LEAVE, 115 SERVER_LEAVE,
  116 + SERVER_LEAVE_FULL_RECONNECT,
116 } 117 }
117 118
118 @Serializable 119 @Serializable
@@ -878,6 +879,7 @@ constructor( @@ -878,6 +879,7 @@ constructor(
878 SimulateScenario.NODE_FAILURE -> builder.nodeFailure = true 879 SimulateScenario.NODE_FAILURE -> builder.nodeFailure = true
879 SimulateScenario.MIGRATION -> builder.migration = true 880 SimulateScenario.MIGRATION -> builder.migration = true
880 SimulateScenario.SERVER_LEAVE -> builder.serverLeave = true 881 SimulateScenario.SERVER_LEAVE -> builder.serverLeave = true
  882 + SimulateScenario.SERVER_LEAVE_FULL_RECONNECT -> builder.leaveRequestFullReconnect = true
881 } 883 }
882 sendSimulateScenario(builder.build()) 884 sendSimulateScenario(builder.build())
883 } 885 }
@@ -540,9 +540,16 @@ constructor( @@ -540,9 +540,16 @@ constructor(
540 } 540 }
541 541
542 fun sendLeave() { 542 fun sendLeave() {
543 - val request = LivekitRtc.SignalRequest.newBuilder()  
544 - .setLeave(LivekitRtc.LeaveRequest.newBuilder().build())  
545 - .build() 543 + val request = with(LivekitRtc.SignalRequest.newBuilder()) {
  544 + leave = with(LivekitRtc.LeaveRequest.newBuilder()) {
  545 + reason = LivekitModels.DisconnectReason.CLIENT_INITIATED
  546 + // server doesn't process this field, keeping it here to indicate the intent of a full disconnect
  547 + action = LivekitRtc.LeaveRequest.Action.DISCONNECT
  548 + build()
  549 + }
  550 + build()
  551 + }
  552 +
546 sendRequest(request) 553 sendRequest(request)
547 } 554 }
548 555
@@ -906,4 +913,7 @@ enum class ProtocolVersion(val value: Int) { @@ -906,4 +913,7 @@ enum class ProtocolVersion(val value: Int) {
906 v10(10), 913 v10(10),
907 v11(11), 914 v11(11),
908 v12(12), 915 v12(12),
  916 +
  917 + // new leave request handling
  918 + v13(13),
909 } 919 }
@@ -393,6 +393,10 @@ class CallViewModel( @@ -393,6 +393,10 @@ class CallViewModel(
393 room.sendSimulateScenario(Room.SimulateScenario.NODE_FAILURE) 393 room.sendSimulateScenario(Room.SimulateScenario.NODE_FAILURE)
394 } 394 }
395 395
  396 + fun simulateServerLeaveFullReconnect() {
  397 + room.sendSimulateScenario(Room.SimulateScenario.SERVER_LEAVE_FULL_RECONNECT)
  398 + }
  399 +
396 fun reconnect() { 400 fun reconnect() {
397 Timber.e { "Reconnecting." } 401 Timber.e { "Reconnecting." }
398 mutablePrimarySpeaker.value = null 402 mutablePrimarySpeaker.value = null
@@ -109,6 +109,7 @@ class CallActivity : AppCompatActivity() { @@ -109,6 +109,7 @@ class CallActivity : AppCompatActivity() {
109 onSendMessage = { viewModel.sendData(it) }, 109 onSendMessage = { viewModel.sendData(it) },
110 onSimulateMigration = { viewModel.simulateMigration() }, 110 onSimulateMigration = { viewModel.simulateMigration() },
111 onSimulateNodeFailure = { viewModel.simulateNodeFailure() }, 111 onSimulateNodeFailure = { viewModel.simulateNodeFailure() },
  112 + onSimulateLeaveFullReconnect = { viewModel.simulateServerLeaveFullReconnect() },
112 fullReconnect = { viewModel.reconnect() }, 113 fullReconnect = { viewModel.reconnect() },
113 ) 114 )
114 } 115 }
@@ -164,6 +165,7 @@ class CallActivity : AppCompatActivity() { @@ -164,6 +165,7 @@ class CallActivity : AppCompatActivity() {
164 onSimulateMigration: () -> Unit = {}, 165 onSimulateMigration: () -> Unit = {},
165 onSimulateNodeFailure: () -> Unit = {}, 166 onSimulateNodeFailure: () -> Unit = {},
166 fullReconnect: () -> Unit = {}, 167 fullReconnect: () -> Unit = {},
  168 + onSimulateLeaveFullReconnect: () -> Unit,
167 ) { 169 ) {
168 AppTheme(darkTheme = true) { 170 AppTheme(darkTheme = true) {
169 ConstraintLayout( 171 ConstraintLayout(
@@ -432,6 +434,7 @@ class CallActivity : AppCompatActivity() { @@ -432,6 +434,7 @@ class CallActivity : AppCompatActivity() {
432 onDismissRequest = { showDebugDialog = false }, 434 onDismissRequest = { showDebugDialog = false },
433 simulateMigration = { onSimulateMigration() }, 435 simulateMigration = { onSimulateMigration() },
434 simulateNodeFailure = { onSimulateNodeFailure() }, 436 simulateNodeFailure = { onSimulateNodeFailure() },
  437 + simulateLeaveFullReconnect = { onSimulateLeaveFullReconnect() },
435 fullReconnect = { fullReconnect() }, 438 fullReconnect = { fullReconnect() },
436 ) 439 )
437 } 440 }
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
1 package io.livekit.android.composesample.ui 17 package io.livekit.android.composesample.ui
2 18
3 import androidx.compose.foundation.background 19 import androidx.compose.foundation.background
4 -import androidx.compose.foundation.layout.* 20 +import androidx.compose.foundation.layout.Column
  21 +import androidx.compose.foundation.layout.Spacer
  22 +import androidx.compose.foundation.layout.fillMaxWidth
  23 +import androidx.compose.foundation.layout.height
  24 +import androidx.compose.foundation.layout.padding
5 import androidx.compose.foundation.shape.RoundedCornerShape 25 import androidx.compose.foundation.shape.RoundedCornerShape
6 import androidx.compose.material.Button 26 import androidx.compose.material.Button
7 import androidx.compose.material.Text 27 import androidx.compose.material.Text
@@ -20,6 +40,7 @@ fun DebugMenuDialog( @@ -20,6 +40,7 @@ fun DebugMenuDialog(
20 simulateMigration: () -> Unit = {}, 40 simulateMigration: () -> Unit = {},
21 fullReconnect: () -> Unit = {}, 41 fullReconnect: () -> Unit = {},
22 simulateNodeFailure: () -> Unit = {}, 42 simulateNodeFailure: () -> Unit = {},
  43 + simulateLeaveFullReconnect: () -> Unit = {},
23 ) { 44 ) {
24 Dialog(onDismissRequest = onDismissRequest) { 45 Dialog(onDismissRequest = onDismissRequest) {
25 Column( 46 Column(
@@ -44,6 +65,14 @@ fun DebugMenuDialog( @@ -44,6 +65,14 @@ fun DebugMenuDialog(
44 }) { 65 }) {
45 Text("Simulate Node Failure") 66 Text("Simulate Node Failure")
46 } 67 }
  68 + Button(
  69 + onClick = {
  70 + simulateLeaveFullReconnect()
  71 + onDismissRequest()
  72 + },
  73 + ) {
  74 + Text("Simulate Server Leave Full Reconnect")
  75 + }
47 Button(onClick = { 76 Button(onClick = {
48 fullReconnect() 77 fullReconnect()
49 onDismissRequest() 78 onDismissRequest()