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/cyan-ways-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": patch
---

Fix resume not working sometimes after connection loss/gain
5 changes: 5 additions & 0 deletions .changeset/shiny-hornets-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": minor
---

Add setting custom reconnect policy
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-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.
Expand All @@ -18,6 +18,7 @@ package io.livekit.android

import io.livekit.android.e2ee.E2EEOptions
import io.livekit.android.room.Room
import io.livekit.android.room.network.ReconnectPolicy
import io.livekit.android.room.participant.AudioTrackPublishDefaults
import io.livekit.android.room.participant.VideoTrackPublishDefaults
import io.livekit.android.room.track.LocalAudioTrackOptions
Expand Down Expand Up @@ -45,4 +46,9 @@ data class RoomOptions(
val videoTrackPublishDefaults: VideoTrackPublishDefaults? = null,
val screenShareTrackCaptureDefaults: LocalVideoTrackOptions? = null,
val screenShareTrackPublishDefaults: VideoTrackPublishDefaults? = null,

/**
* @see [Room.reconnectPolicy]
*/
val reconnectPolicy: ReconnectPolicy? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import io.livekit.android.e2ee.E2EEManager
import io.livekit.android.e2ee.EncryptedPacket
import io.livekit.android.events.DisconnectReason
import io.livekit.android.events.convert
import io.livekit.android.room.network.DefaultReconnectPolicy
import io.livekit.android.room.network.ReconnectContext
import io.livekit.android.room.network.ReconnectPolicy
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.ParticipantTrackPermission
import io.livekit.android.room.track.TrackException
Expand Down Expand Up @@ -99,6 +102,7 @@ import javax.inject.Singleton
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

/**
Expand Down Expand Up @@ -158,6 +162,8 @@ internal constructor(
@Volatile
private var fullReconnectOnNext = false

internal var reconnectPolicy: ReconnectPolicy = DefaultReconnectPolicy()

private val pendingTrackResolvers: MutableMap<String, Continuation<LivekitModels.TrackInfo>> =
mutableMapOf()

Expand Down Expand Up @@ -526,6 +532,8 @@ internal constructor(
var hasReconnectedOnce = false

val reconnectStartTime = SystemClock.elapsedRealtime()
val reconnectPolicy = this@RTCEngine.reconnectPolicy

for (retries in 0 until MAX_RECONNECT_RETRIES) {
// First try use previously valid url.
if (retries != 0) {
Expand All @@ -546,9 +554,14 @@ internal constructor(
break
}

var startDelay = 100 + retries.toLong() * retries * 500
if (startDelay > 5000) {
startDelay = 5000
val reconnectContext = ReconnectContext(
retryCount = retries,
elapsedTime = (SystemClock.elapsedRealtime() - reconnectStartTime).milliseconds,
)
val startDelay = reconnectPolicy.getNextRetryDelay(reconnectContext)
if (startDelay == null) {
LKLog.i { "cancelling reconnection due to policy." }
break
}

LKLog.i { "Reconnecting to signal, attempt ${retries + 1}" }
Expand Down Expand Up @@ -644,9 +657,15 @@ internal constructor(
break
}

if (connectionState == ConnectionState.CONNECTED &&
(!hasPublished || publisher?.isConnected() == true)
val subscriberConnected = subscriber?.isConnected() == true
val publisherConnected = !hasPublished || publisher?.isConnected() == true
if ((connectionState == ConnectionState.CONNECTED || connectionState == ConnectionState.RESUMING) &&
subscriberConnected &&
publisherConnected
) {
if (connectionState == ConnectionState.RESUMING) {
connectionState = ConnectionState.CONNECTED
}
if (lastMessageSeq != null) {
resendReliableMessagesForResume(lastMessageSeq)
}
Expand Down Expand Up @@ -979,7 +998,7 @@ internal constructor(
@VisibleForTesting
const val LOSSY_DATA_CHANNEL_LABEL = "_lossy"
internal const val MAX_DATA_PACKET_SIZE = 15 * 1024 // 15 KB
private const val MAX_RECONNECT_RETRIES = 10
private const val MAX_RECONNECT_RETRIES = 30
private const val MAX_RECONNECT_TIMEOUT = 60 * 1000
private const val MAX_ICE_CONNECT_TIMEOUT_MS = 20000

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.datastream.incoming.IncomingDataStreamManager
import io.livekit.android.room.metrics.collectMetrics
import io.livekit.android.room.network.NetworkCallbackManagerFactory
import io.livekit.android.room.network.ReconnectPolicy
import io.livekit.android.room.participant.AudioTrackPublishDefaults
import io.livekit.android.room.participant.ConnectionQuality
import io.livekit.android.room.participant.LocalParticipant
Expand Down Expand Up @@ -317,6 +318,11 @@ constructor(
*/
var screenShareTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::screenShareTrackPublishDefaults

/**
* [ReconnectPolicy] to use when reconnecting to the server.
*/
var reconnectPolicy: ReconnectPolicy by engine::reconnectPolicy

val localParticipant: LocalParticipant = localParticipantFactory.create(dynacast = false).apply {
internalListener = this@Room
}
Expand Down Expand Up @@ -613,6 +619,9 @@ constructor(
options.screenShareTrackPublishDefaults?.let {
screenShareTrackPublishDefaults = it
}
options.reconnectPolicy?.let {
reconnectPolicy = it
}
adaptiveStream = options.adaptiveStream
dynacast = options.dynacast
e2eeOptions = options.e2eeOptions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.network

import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

/**
* A reconnect policy that takes in a list of delays to iterate through.
*/
class DefaultReconnectPolicy(
/**
* The list of delays to use. If the number of retries exceeds the size of the list,
* reconnection is cancelled.
*
* Defaults to aggressively retrying multiple times before exponentially backing off, up to 5 seconds.
*/
val retryDelays: List<Duration> = DEFAULT_RETRY_DELAYS,
/**
* The max total time to try reconnecting. Defaults to 60 seconds.
*/
val maxReconnectionTimeout: Duration = DEFAULT_MAX_RECONNECTION_TIMEOUT,
) : ReconnectPolicy {
override fun getNextRetryDelay(context: ReconnectContext): Duration? {
if (context.retryCount >= retryDelays.size) {
return null
}

if (context.elapsedTime > maxReconnectionTimeout) {
return null
}

return retryDelays[context.retryCount]
}

companion object {

val DEFAULT_MAX_RECONNECTION_TIMEOUT = 60.seconds

val DEFAULT_MAX_RETRY_DELAY = 5.seconds

val DEFAULT_RETRY_DELAYS = listOf(
100.milliseconds,
300.milliseconds, // Aggressively try to reconnect a couple of times. Wifi -> LTE handoff can randomly take a while.
300.milliseconds,
500.milliseconds,
500.milliseconds,
500.milliseconds,
(2 * 2 * 300).milliseconds,
(3 * 3 * 300).milliseconds,
(4 * 4 * 300).milliseconds,
DEFAULT_MAX_RETRY_DELAY,
DEFAULT_MAX_RETRY_DELAY,
DEFAULT_MAX_RETRY_DELAY,
DEFAULT_MAX_RETRY_DELAY,
DEFAULT_MAX_RETRY_DELAY,
DEFAULT_MAX_RETRY_DELAY,
DEFAULT_MAX_RETRY_DELAY,
DEFAULT_MAX_RETRY_DELAY,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.network

import kotlin.time.Duration

/**
* Policy for reconnections that determines the delay between retries.
*/
interface ReconnectPolicy {
/**
* Called after a disconnect is detected, and between each reconnect attempt.
*
* Note: To prevent infinitely retrying, there is a hard cap of 30 retries, regardless of policy.
*
* @return The [Duration] to delay before the next reconnect attempt, or null to cancel reconnections.
*
*/
fun getNextRetryDelay(context: ReconnectContext): Duration?
}

data class ReconnectContext(
/**
* The number of failed reconnect attempts. 0 means this is the first reconnect attempt.
*/
val retryCount: Int,

/**
* Elapsed amount of time in milliseconds since the disconnect.
*/
val elapsedTime: Duration,
)
Loading