Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d3365f5
refactor(core): add retryable signaling decorator, retry policies, an…
PratimMallick Apr 13, 2026
1d7596a
fix(core): phased migration to prevent old-session race condition
PratimMallick Apr 13, 2026
b9bf598
refactor(core): add unified reconnect loop with strategy escalation
PratimMallick Apr 13, 2026
8def70d
refactor(core): simplify RtcSession reconnect delegation
PratimMallick Apr 13, 2026
6bf2b55
test(core): update tests for unified reconnect architecture
PratimMallick Apr 13, 2026
156c3de
fix(core): align reconnect guard with JS SDK — allow migrate while co…
PratimMallick Apr 13, 2026
d1f5b55
fix(core): prevent reconnect race and clean up migration/fast-reconnect
PratimMallick Apr 15, 2026
58d2bd4
fix(core): reliable network transition handling without dropping calls
PratimMallick Apr 15, 2026
27a3bbf
api dump and spotless changes
PratimMallick Apr 15, 2026
20af1ce
Fix - catch (Exception) swallows CancellationException in reconnect loop
PratimMallick Apr 17, 2026
1a09c75
Fix - session.value!! force-unwrap TOCTOU race in reconnectRejoin/rec…
PratimMallick Apr 20, 2026
58b9d59
spotless apply
PratimMallick Apr 21, 2026
65586f2
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick Apr 21, 2026
cde0dd8
Typo fixed
PratimMallick Apr 21, 2026
92f3ebe
Fix: Added catch (e: CancellationException) { throw e } before the ge…
PratimMallick Apr 21, 2026
d5a52a0
Adding back the sfuReconnectTimeoutMillis so that there's no breaking…
PratimMallick Apr 21, 2026
d026ed2
Fixed nitpicks by coderabbit
PratimMallick Apr 21, 2026
5788b8d
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick Apr 21, 2026
d3fa38c
import fixed
PratimMallick Apr 21, 2026
bd77b9e
Renamed the local variable to loopStartTime
PratimMallick Apr 22, 2026
bfa174e
Renamed the local variable to loopIteration
PratimMallick Apr 22, 2026
60b26d8
Add comments
PratimMallick Apr 22, 2026
f9e7bc9
All precondition guards in reconnectFast, reconnectRejoin, reconnectM…
PratimMallick Apr 22, 2026
580549f
refactor(core): replace exception-driven reconnect with sealed Reconn…
PratimMallick Apr 22, 2026
01c22fb
refactor(core): introduce SfuConnectionResult and unify SFU connect p…
PratimMallick Apr 22, 2026
f33ab4e
refactor(core): fix sendLeaveEvent ordering, deprecate connect, use i…
PratimMallick Apr 23, 2026
45c8a3f
fix(core): add ICE restart after fast reconnect and gate retries on n…
PratimMallick Apr 23, 2026
0c9d26d
Api dump
PratimMallick Apr 23, 2026
4c6f159
refactor(core): unify SFU reconnection under Call.reconnect() and fix…
PratimMallick Apr 24, 2026
e2e213a
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick Apr 24, 2026
1472e7b
test(core): fix ReconnectEscalationTest assertions after leave-on-fai…
PratimMallick Apr 24, 2026
bc5b391
test(core): fix test isolation and add internalConnect tests
PratimMallick Apr 27, 2026
feb4231
refactor(core): unify SFU error handling and add ICE health monitoring
PratimMallick Apr 30, 2026
a43055c
fix(core): defer ICE monitoring start until SFU socket is connected
PratimMallick Apr 30, 2026
9661fc7
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick Apr 30, 2026
70e8b70
refactor(core): address PR review — rename APIs, unify DISCONNECT flo…
PratimMallick Apr 30, 2026
1998f38
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick Apr 30, 2026
9d35abf
- Donot call reconnect on getting DisconnectedPermanently
PratimMallick Apr 30, 2026
a12bdd9
fix(core): centralize network check in reconnect loop, fail-fast on s…
PratimMallick Apr 30, 2026
11e4306
fix: Correctly update currentStrategy after reconnectDeadlineMillis h…
rahul-lohra May 1, 2026
a250b33
- move the logic of incrementing nonFastReconnectAttempts inside the …
PratimMallick May 1, 2026
24d05e6
spotlessApply
PratimMallick May 1, 2026
411b971
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick May 1, 2026
7f8e79d
Fix unit test cases
PratimMallick May 1, 2026
3c82c67
Make isClosed internal
PratimMallick May 1, 2026
2a26a66
FIx unit test
PratimMallick May 1, 2026
d647d6d
add a 5s delay before asserting recording label reappearance
aleksandar-apostolov May 1, 2026
d4f6204
Merge remote-tracking branch 'origin/fix/cleanup-old-rtc-session-on-m…
aleksandar-apostolov May 1, 2026
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
7 changes: 7 additions & 0 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -9308,6 +9308,13 @@ public final class io/getstream/video/android/core/RealtimeConnection$Reconnecti
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/RealtimeConnection$ReconnectingFailed : io/getstream/video/android/core/RealtimeConnection {
public static final field INSTANCE Lio/getstream/video/android/core/RealtimeConnection$ReconnectingFailed;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public abstract interface class io/getstream/video/android/core/RingingState {
}

Expand Down
2 changes: 1 addition & 1 deletion stream-video-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ android {

resourcePrefix = "stream_video_"

sourceSets.configureEach {
sourceSets.getByName("main") {
kotlin.srcDir("${project.buildDir}/generated/source/services")
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ public sealed interface RealtimeConnection {

public data object Migrating : RealtimeConnection
public data class Failed(val error: Any) : RealtimeConnection // permanent failure
public data object ReconnectingFailed : RealtimeConnection // all retry attempts exhausted
public data object Disconnected : RealtimeConnection // normal disconnect by the app
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import android.media.AudioRecord.READ_BLOCKING
import android.media.projection.MediaProjection
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,7 @@ internal class Publisher(
* This will cause emission of ParticipantLeftEvent to other person
*/
ErrorCode.ERROR_CODE_REQUEST_VALIDATION_FAILED -> rejoin()
ErrorCode.ERROR_CODE_PARTICIPANT_NOT_FOUND -> {}
ErrorCode.ERROR_CODE_PARTICIPANT_SIGNAL_LOST -> {
// should fast-reconnect but wait for further investigation
}

else -> {}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@ internal class Subscriber(

/**
* Restarts the ICE connection with the SFU.
* Terminal errors (e.g. PARTICIPANT_NOT_FOUND) are propagated by the
* signaling decorator and trigger a rejoin; we cancel scheduled retries
* so this session stops attempting recovery on a dead SFU participant.
*/
suspend fun restartIce() = wrapAPICall {
val request = ICERestartRequest(
Expand All @@ -339,6 +342,7 @@ internal class Subscriber(
sfuClient.iceRestart(request)
}.onError {
tracer.trace("iceRestart-error", it.message ?: "unknown")
restartIceJobDelegate.cancelScheduledRestartIce()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* 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.getstream.video.android.core.call.utils

import io.getstream.video.android.core.api.SignalServerService
import io.getstream.video.android.core.retry.StreamRetryPolicy
import io.getstream.video.android.core.retry.StreamRetryProcessor
import stream.video.sfu.models.Error
import stream.video.sfu.models.ErrorCode
import stream.video.sfu.models.ICETrickle
import stream.video.sfu.signal.ICERestartRequest
import stream.video.sfu.signal.ICERestartResponse
import stream.video.sfu.signal.ICETrickleResponse
import stream.video.sfu.signal.SendAnswerRequest
import stream.video.sfu.signal.SendAnswerResponse
import stream.video.sfu.signal.SendStatsRequest
import stream.video.sfu.signal.SendStatsResponse
import stream.video.sfu.signal.SetPublisherRequest
import stream.video.sfu.signal.SetPublisherResponse
import stream.video.sfu.signal.StartNoiseCancellationRequest
import stream.video.sfu.signal.StartNoiseCancellationResponse
import stream.video.sfu.signal.StopNoiseCancellationRequest
import stream.video.sfu.signal.StopNoiseCancellationResponse
import stream.video.sfu.signal.UpdateMuteStatesRequest
import stream.video.sfu.signal.UpdateMuteStatesResponse
import stream.video.sfu.signal.UpdateSubscriptionsRequest
import stream.video.sfu.signal.UpdateSubscriptionsResponse

/**
* SFU response error wrapper that lets the retry loop inspect `should_retry`
* without knowing the concrete response type.
*/
internal class SfuRetryableException(val sfuError: Error) : Exception(sfuError.message)

/**
* Decorator that retries SFU API calls on transient failures and propagates
* errors to callbacks for reconnection handling.
*
* **Retry behaviour** (via [StreamRetryProcessor]):
* - Network errors (IOException, timeout) are retried automatically.
* - SFU responses with `should_retry = true` are retried (except SIGNAL_LOST,
* which needs a WebSocket reconnect, not an HTTP retry).
* - Retries stop on success, terminal error (`should_retry = false`),
* max attempts exhausted, or coroutine cancellation.
*
* **Error propagation**:
* - SFU response errors → [onTerminalError] (maps to fastReconnect / rejoin).
* - Network errors after all retries exhausted → [onNetworkFailure] (triggers
* reconnect since the SFU is unreachable via HTTP).
*/
internal class RetryableSignalingServiceDecorator(
private val decorated: SignalServerService,
private val onTerminalError: (Error) -> Unit = {},
private val onNetworkFailure: (Throwable) -> Unit = {},
private val retryProcessor: StreamRetryProcessor = StreamRetryProcessor("SfuRetry"),
private val policy: StreamRetryPolicy = StreamRetryPolicy.linear(
minRetries = 1,
maxRetries = 3,
backoffStepMillis = 250,
maxBackoffMillis = 2_000,
initialDelayMillis = 0,
),
) : SignalServerService {

override suspend fun setPublisher(setPublisherRequest: SetPublisherRequest): SetPublisherResponse =
retryCall({ it.error }) { decorated.setPublisher(setPublisherRequest) }

override suspend fun sendAnswer(sendAnswerRequest: SendAnswerRequest): SendAnswerResponse =
retryCall({ it.error }) { decorated.sendAnswer(sendAnswerRequest) }

override suspend fun iceTrickle(iceTrickle: ICETrickle): ICETrickleResponse =
retryCall({ it.error }) { decorated.iceTrickle(iceTrickle) }

override suspend fun updateSubscriptions(
updateSubscriptionsRequest: UpdateSubscriptionsRequest,
): UpdateSubscriptionsResponse =
retryCall({ it.error }) { decorated.updateSubscriptions(updateSubscriptionsRequest) }

override suspend fun updateMuteStates(updateMuteStatesRequest: UpdateMuteStatesRequest): UpdateMuteStatesResponse =
retryCall({ it.error }) { decorated.updateMuteStates(updateMuteStatesRequest) }

override suspend fun iceRestart(iceRestartRequest: ICERestartRequest): ICERestartResponse =
retryCall({ it.error }) { decorated.iceRestart(iceRestartRequest) }

override suspend fun sendStats(sendStatsRequest: SendStatsRequest): SendStatsResponse =
retryCall({ it.error }) { decorated.sendStats(sendStatsRequest) }

override suspend fun startNoiseCancellation(startNoiseCancellationRequest: StartNoiseCancellationRequest): StartNoiseCancellationResponse =
retryCall({ it.error }) { decorated.startNoiseCancellation(startNoiseCancellationRequest) }

override suspend fun stopNoiseCancellation(stopNoiseCancellationRequest: StopNoiseCancellationRequest): StopNoiseCancellationResponse =
retryCall({ it.error }) { decorated.stopNoiseCancellation(stopNoiseCancellationRequest) }

/**
* Wraps an SFU API call with retry + error propagation.
*
* Network errors throw naturally and are retried by the processor.
* SFU-level `should_retry=true` errors are converted to [SfuRetryableException]
* to re-enter the retry loop (except SIGNAL_LOST which requires WS reconnect).
*
* After the final outcome:
* - SFU response with error → [onTerminalError]
* - Network failure after all retries → [onNetworkFailure], then rethrow
*/
private suspend inline fun <T> retryCall(
crossinline errorExtractor: (T) -> Error?,
crossinline block: suspend () -> T,
): T {
var lastResponse: T? = null

val result = retryProcessor.retry(policy) {
val response = block()
lastResponse = response
val error = errorExtractor(response)
if (error != null && error.should_retry && !error.requiresReconnect()) {
throw SfuRetryableException(error)
Comment thread
rahul-lohra marked this conversation as resolved.
Comment thread
PratimMallick marked this conversation as resolved.
}
response
}

val response = result.getOrElse { throwable ->
if (throwable is SfuRetryableException && lastResponse != null) {
@Suppress("UNCHECKED_CAST")
lastResponse as T
} else {
onNetworkFailure(throwable)
throw throwable
}
}

errorExtractor(response)?.let { onTerminalError(it) }

return response
}

private fun Error.requiresReconnect(): Boolean =
code == ErrorCode.ERROR_CODE_PARTICIPANT_SIGNAL_LOST
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import android.content.Context
import android.net.ConnectivityManager
import androidx.lifecycle.Lifecycle
import io.getstream.video.android.core.api.SignalServerService
import io.getstream.video.android.core.call.utils.SignalLostSignalingServiceDecorator
import io.getstream.video.android.core.call.utils.RetryableSignalingServiceDecorator
import io.getstream.video.android.core.internal.network.NetworkStateProvider
import io.getstream.video.android.core.socket.common.token.ConstantTokenProvider
import io.getstream.video.android.core.socket.common.token.TokenRepository
Expand All @@ -45,7 +45,8 @@ internal class SfuConnectionModule(
override val connectionTimeoutInMs: Long,
override val lifecycle: Lifecycle,
override val tracer: Tracer,
val onSignalingLost: (Error) -> Unit,
val onSfuApiError: (Error) -> Unit,
val onSfuNetworkFailure: (Throwable) -> Unit,
Comment thread
rahul-lohra marked this conversation as resolved.
Outdated
) : ConnectionModuleDeclaration<SignalServerService, SfuSocketConnection, OkHttpClient, SfuToken> {

// Internal logic
Expand All @@ -70,17 +71,16 @@ internal class SfuConnectionModule(
.callTimeout(connectionTimeoutInMs, TimeUnit.MILLISECONDS).build()
}

// API
override val api: SignalServerService = SignalLostSignalingServiceDecorator(
tracedWith(
signalRetrofitClient.create(
SignalServerService::class.java,
),
// API – decorator chain: Retrofit → Trace → Retry (+ error propagation)
// Tracer wraps Retrofit so every attempt (including retries) is traced.
override val api: SignalServerService = RetryableSignalingServiceDecorator(
decorated = tracedWith(
signalRetrofitClient.create(SignalServerService::class.java),
tracer,
),
) {
onSignalingLost(it)
}
onTerminalError = { onSfuApiError(it) },
onNetworkFailure = { onSfuNetworkFailure(it) },
)
override val networkStateProvider: NetworkStateProvider by lazy {
NetworkStateProvider(
scope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ public class NetworkStateProvider(

override fun onLost(network: Network) {
logger.d { "[callback#onLost] #network; onLost" }
isConnected = false
listeners.onDisconnected()
notifyListenersIfNetworkStateChanged()
Comment thread
aleksandar-apostolov marked this conversation as resolved.
Outdated
}
}

Expand Down
Loading
Loading