diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt index c2d0393b34..0d3da53a37 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt @@ -112,7 +112,6 @@ class DirectCallActivity : ComponentActivity() { } is CancelCall -> { lifecycleScope.launch { - val test = call.reject() call.leave() finish() } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt b/demo-app/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt index 4d86f86bc3..9e327ebc0f 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt @@ -21,6 +21,7 @@ import android.util.Log import com.google.android.gms.auth.GoogleAuthUtil import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter @@ -29,7 +30,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.getstream.video.android.data.dto.GetGoogleAccountsResponseDto import io.getstream.video.android.data.dto.asDomainModel import io.getstream.video.android.models.GoogleAccount -import io.getstream.video.android.util.GoogleSignInHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext @@ -40,6 +40,7 @@ import javax.inject.Inject class GoogleAccountRepository @Inject constructor( @ApplicationContext private val context: Context, + private val googleSignInClient: GoogleSignInClient, ) { private val baseUrl = "https://people.googleapis.com/v1/people:listDirectoryPeople" @@ -76,8 +77,7 @@ class GoogleAccountRepository @Inject constructor( } private suspend fun signInSilently(): Boolean { - val gsc = GoogleSignInHelper.getGoogleSignInClient(context) - val task = gsc.silentSignIn() + val task = googleSignInClient.silentSignIn() return if (task.isSuccessful) { Log.d("Google Silent Sign In", "Successful") diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/di/AppModule.kt b/demo-app/src/main/kotlin/io/getstream/video/android/di/AppModule.kt index fe3e20bca3..01404b2319 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/di/AppModule.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/di/AppModule.kt @@ -17,10 +17,15 @@ package io.getstream.video.android.di import android.content.Context +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.Scope import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import io.getstream.video.android.R import io.getstream.video.android.data.repositories.GoogleAccountRepository import io.getstream.video.android.datastore.delegate.StreamUserDataStore import javax.inject.Singleton @@ -35,8 +40,20 @@ object AppModule { return StreamUserDataStore.instance() } + @Provides + @Singleton + fun provideGoogleSignInClient( + @ApplicationContext context: Context, + ): GoogleSignInClient = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + .requestIdToken(context.getString(R.string.default_web_client_id)) + .requestScopes(Scope("https://www.googleapis.com/auth/directory.readonly")) + .build() + .let { gso -> GoogleSignIn.getClient(context, gso) } + @Provides fun provideGoogleAccountRepository( @ApplicationContext context: Context, - ) = GoogleAccountRepository(context) + googleSignInClient: GoogleSignInClient, + ) = GoogleAccountRepository(context, googleSignInClient) } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/models/StreamUser.kt b/demo-app/src/main/kotlin/io/getstream/video/android/models/StreamUser.kt deleted file mode 100644 index 1dbb21bc8a..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/models/StreamUser.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2014-2023 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.models - -data class StreamUser( - val email: String, - val id: String, - val name: String, - val avatarUrl: String?, - val isFavorite: Boolean, -) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 4542c79220..f006d6b295 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -65,6 +65,7 @@ import io.getstream.video.android.core.RealtimeConnection import io.getstream.video.android.core.call.state.ChooseLayout import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall +import io.getstream.video.android.tooling.util.StreamFlavors import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -120,7 +121,8 @@ fun CallScreen( call = call, layout = layout, enableInPictureInPicture = true, - enableDiagnostics = BuildConfig.DEBUG, + enableDiagnostics = BuildConfig.DEBUG || + BuildConfig.FLAVOR == StreamFlavors.development, onCallAction = { when (it) { ChooseLayout -> isShowingLayoutChooseMenu = true diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt index 40b257c74a..1e6d07e2ec 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt @@ -75,6 +75,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions import io.getstream.video.android.BuildConfig import io.getstream.video.android.R import io.getstream.video.android.compose.theme.VideoTheme @@ -82,6 +84,7 @@ import io.getstream.video.android.compose.ui.components.avatar.UserAvatar import io.getstream.video.android.datastore.delegate.StreamUserDataStore import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewUsers +import io.getstream.video.android.model.User import io.getstream.video.android.tooling.util.StreamFlavors import io.getstream.video.android.ui.theme.Colors import io.getstream.video.android.ui.theme.StreamButton @@ -95,6 +98,8 @@ fun CallJoinScreen( navigateToBarcodeScanner: () -> Unit = {}, ) { val uiState by callJoinViewModel.uiState.collectAsState(CallJoinUiState.Nothing) + val user by callJoinViewModel.user.collectAsState(initial = null) + var isSignOutDialogVisible by remember { mutableStateOf(false) } val isLoggedOut by callJoinViewModel.isLoggedOut.collectAsState(initial = false) @@ -111,8 +116,8 @@ fun CallJoinScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { CallJoinHeader( + user = user, onAvatarLongClick = { isSignOutDialogVisible = true }, - callJoinViewModel = callJoinViewModel, onDirectCallClick = navigateToDirectCallJoin, onSignOutClick = { callJoinViewModel.autoLogInAfterLogOut = false @@ -173,13 +178,11 @@ private fun HandleCallJoinUiState( @OptIn(ExperimentalFoundationApi::class) @Composable private fun CallJoinHeader( - callJoinViewModel: CallJoinViewModel = hiltViewModel(), + user: User?, onAvatarLongClick: () -> Unit, onDirectCallClick: () -> Unit, onSignOutClick: () -> Unit, ) { - val user by callJoinViewModel.user.collectAsState(initial = null) - Row( modifier = Modifier .fillMaxWidth() @@ -442,7 +445,13 @@ private fun CallJoinScreenPreview() { VideoTheme { StreamUserDataStore.install(LocalContext.current) CallJoinScreen( - callJoinViewModel = CallJoinViewModel(StreamUserDataStore.instance()), + callJoinViewModel = CallJoinViewModel( + dataStore = StreamUserDataStore.instance(), + googleSignInClient = GoogleSignIn.getClient( + LocalContext.current, + GoogleSignInOptions.Builder().build(), + ), + ), navigateToCallLobby = {}, navigateUpToLogin = {}, navigateToDirectCallJoin = {}, diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinViewModel.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinViewModel.kt index b122a2e7df..826ff4d0e9 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinViewModel.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinViewModel.kt @@ -18,7 +18,7 @@ package io.getstream.video.android.ui.join import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.firebase.auth.FirebaseAuth +import com.google.android.gms.auth.api.signin.GoogleSignInClient import dagger.hilt.android.lifecycle.HiltViewModel import io.getstream.chat.android.client.ChatClient import io.getstream.video.android.core.Call @@ -43,6 +43,7 @@ import javax.inject.Inject @HiltViewModel class CallJoinViewModel @Inject constructor( private val dataStore: StreamUserDataStore, + private val googleSignInClient: GoogleSignInClient, ) : ViewModel() { val user: Flow = dataStore.user val isLoggedOut = dataStore.user.map { it == null } @@ -97,7 +98,7 @@ class CallJoinViewModel @Inject constructor( fun logOut() { viewModelScope.launch { - FirebaseAuth.getInstance().signOut() + googleSignInClient.signOut() dataStore.clear() StreamVideo.instance().logOut() ChatClient.instance().disconnect(true).enqueue() diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt index 5703b8979b..5d6c0ac4dc 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt @@ -59,6 +59,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions import io.getstream.video.android.BuildConfig import io.getstream.video.android.R import io.getstream.video.android.compose.theme.VideoTheme @@ -322,6 +324,10 @@ private fun CallLobbyScreenPreview() { mapOf("cid" to "default:123"), ), dataStore = StreamUserDataStore.instance(), + googleSignInClient = GoogleSignIn.getClient( + LocalContext.current, + GoogleSignInOptions.Builder().build(), + ), ), ) {} } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt index a56c6e4465..a9e804e270 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt @@ -20,7 +20,7 @@ import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.firebase.auth.FirebaseAuth +import com.google.android.gms.auth.api.signin.GoogleSignInClient import dagger.hilt.android.lifecycle.HiltViewModel import io.getstream.chat.android.client.ChatClient import io.getstream.video.android.core.Call @@ -49,6 +49,7 @@ import javax.inject.Inject class CallLobbyViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val dataStore: StreamUserDataStore, + private val googleSignInClient: GoogleSignInClient, ) : ViewModel() { private val cid: String = checkNotNull(savedStateHandle["cid"]) @@ -167,7 +168,7 @@ class CallLobbyViewModel @Inject constructor( fun signOut() { viewModelScope.launch { - FirebaseAuth.getInstance().signOut() + googleSignInClient.signOut() dataStore.clear() StreamVideo.instance().logOut() ChatClient.instance().disconnect(true).enqueue() diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt index 74ee02b6ed..a4e45c7e4b 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt @@ -72,7 +72,6 @@ import io.getstream.video.android.ui.theme.Colors import io.getstream.video.android.ui.theme.LinkText import io.getstream.video.android.ui.theme.LinkTextData import io.getstream.video.android.ui.theme.StreamButton -import io.getstream.video.android.util.GoogleSignInHelper import io.getstream.video.android.util.UserHelper /** @@ -344,9 +343,7 @@ private fun HandleLoginUiStates( LaunchedEffect(key1 = loginUiState) { when (loginUiState) { is LoginUiState.GoogleSignIn -> { - signInLauncher.launch( - GoogleSignInHelper.getGoogleSignInClient(context).signInIntent, - ) + signInLauncher.launch(loginUiState.signInIntent) } is LoginUiState.SignInComplete -> { diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt index fdfe8fff14..6c061443dd 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt @@ -16,8 +16,10 @@ package io.getstream.video.android.ui.login +import android.content.Intent import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.android.gms.auth.api.signin.GoogleSignInClient import dagger.hilt.android.lifecycle.HiltViewModel import io.getstream.log.streamLog import io.getstream.video.android.API_KEY @@ -48,6 +50,7 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( private val dataStore: StreamUserDataStore, + private val googleSignInClient: GoogleSignInClient, private val googleAccountRepository: GoogleAccountRepository, ) : ViewModel() { var autoLogIn: Boolean = true @@ -57,7 +60,11 @@ class LoginViewModel @Inject constructor( .flatMapLatest { event -> when (event) { is LoginEvent.Loading -> flowOf(LoginUiState.Loading) - is LoginEvent.GoogleSignIn -> flowOf(LoginUiState.GoogleSignIn) + is LoginEvent.GoogleSignIn -> flowOf( + LoginUiState.GoogleSignIn( + signInIntent = googleSignInClient.signInIntent, + ), + ) is LoginEvent.SignInSuccess -> signInSuccess(event.userId) is LoginEvent.SignInFailure -> flowOf( LoginUiState.SignInFailure(event.errorMessage), @@ -138,10 +145,10 @@ sealed interface LoginUiState { object Loading : LoginUiState - object GoogleSignIn : LoginUiState - object AlreadyLoggedIn : LoginUiState + data class GoogleSignIn(val signInIntent: Intent) : LoginUiState + data class SignInComplete(val tokenResponse: TokenResponse) : LoginUiState data class SignInFailure(val errorMessage: String) : LoginUiState diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/GoogleSignInHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/GoogleSignInHelper.kt deleted file mode 100644 index 0a7399064b..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/GoogleSignInHelper.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2014-2023 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.util - -import android.content.Context -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInClient -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.common.api.Scope -import io.getstream.video.android.R - -object GoogleSignInHelper { - fun getGoogleSignInClient(context: Context): GoogleSignInClient { - val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestEmail() - .requestIdToken(context.getString(R.string.default_web_client_id)) - .requestScopes(Scope("https://www.googleapis.com/auth/directory.readonly")) - .build() - - return GoogleSignIn.getClient(context, gso) - } -} diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index f46198dd4a..5f5dc27a8d 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -93,6 +93,7 @@ public final class io/getstream/video/android/core/CallHealthMonitor { public final fun reconnect (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun start ()V public final fun stop ()V + public final fun stopTimer ()V } public final class io/getstream/video/android/core/CallKt { @@ -4175,8 +4176,8 @@ public final class io/getstream/video/android/core/socket/ErrorResponse$Companio public class io/getstream/video/android/core/socket/PersistentSocket : okhttp3/WebSocketListener { public field connected Lkotlinx/coroutines/CancellableContinuation; - public fun (Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V protected final fun ackHealthMonitor ()V public fun authenticate ()V public final fun cleanup ()V @@ -4226,8 +4227,8 @@ public final class io/getstream/video/android/core/socket/PersistentSocket$Disco } public final class io/getstream/video/android/core/socket/SfuSocket : io/getstream/video/android/core/socket/PersistentSocket { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlinx/coroutines/CoroutineScope;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlinx/coroutines/CoroutineScope;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlinx/coroutines/CoroutineScope;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlinx/coroutines/CoroutineScope;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun authenticate ()V public fun connect (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun connectMigrating (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallHealthMonitor.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallHealthMonitor.kt index 4d4ccfcfa3..9aa195adaa 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallHealthMonitor.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallHealthMonitor.kt @@ -57,7 +57,7 @@ public class CallHealthMonitor( private val checkInterval = 5000L private var lastReconnectAt: OffsetDateTime? = null private val reconnectDebounceMs = 700L - private val iceRestartTimeout = 4000L + private val iceRestartTimeout = 6000L private var isRunning = false private var timeoutJob: Job? = null @@ -93,6 +93,10 @@ public class CallHealthMonitor( network.unsubscribe(networkStateListener) } + fun stopTimer() { + timeoutJob?.cancel() + } + val goodStates = listOf( PeerConnection.PeerConnectionState.NEW, // New is good, means we're not using it yet PeerConnection.PeerConnectionState.CONNECTED, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt index 5e95e1cfd1..90883a140a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt @@ -63,6 +63,8 @@ import io.getstream.video.android.core.utils.stringify import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -301,12 +303,11 @@ public class RtcSession internal constructor( private val _sfuSocketState = MutableStateFlow(SocketState.NotConnected) val sfuSocketState = _sfuSocketState.asStateFlow() - private val sfuFastReconnectListener: () -> Unit = { + private val sfuFastReconnectListener: suspend () -> Unit = { // SFU socket has done a fast-reconnect. We need to an ICE restart immediately and not wait // until the health check runs - coroutineScope.launch { - call.monitor.reconnect(forceRestart = true) - } + call.monitor.stopTimer() + call.monitor.reconnect(forceRestart = true) } init { @@ -391,18 +392,25 @@ public class RtcSession internal constructor( */ suspend fun reconnect(forceRestart: Boolean) { // ice restart - subscriber?.let { - if (!it.isHealthy()) { - logger.i { "ice restarting subscriber peer connection" } - requestSubscriberIceRestart() + val subscriberAsync = coroutineScope.async { + subscriber?.let { + if (!it.isHealthy()) { + logger.i { "ice restarting subscriber peer connection" } + requestSubscriberIceRestart() + } } } - publisher?.let { - if (!it.isHealthy() || forceRestart) { - logger.i { "ice restarting publisher peer connection (force restart = $forceRestart)" } - it.connection.restartIce() + + val publisherAsync = coroutineScope.async { + publisher?.let { + if (!it.isHealthy() || forceRestart) { + logger.i { "ice restarting publisher peer connection (force restart = $forceRestart)" } + it.connection.restartIce() + } } } + + awaitAll(subscriberAsync, publisherAsync) } suspend fun connect() { @@ -441,10 +449,14 @@ public class RtcSession internal constructor( sfuConnectionModule.sfuSocket.connectionState.collect { sfuSocketState -> _sfuSocketState.value = sfuSocketState - // make sure we stop handling subscriber ICE candidates when a new SFU socket + // make sure we stop handling ICE candidates when a new SFU socket // connection is being established. We need to wait until a SubscriberOffer // is received again and then we start listening to the ICE candidate queue - if (sfuSocketState == SocketState.Connecting) { + if (sfuSocketState == SocketState.Connecting || + sfuSocketState is SocketState.DisconnectedTemporarily || + sfuSocketState is SocketState.DisconnectedByRequest + ) { + syncSubscriberCandidates?.cancel() syncSubscriberCandidates?.cancel() } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/ConnectionModule.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/ConnectionModule.kt index cfa30e6bfe..881f7f4b99 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/ConnectionModule.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/ConnectionModule.kt @@ -142,7 +142,7 @@ internal class ConnectionModule( sessionId: String, sfuToken: String, getSubscriberSdp: suspend () -> String, - onFastReconnect: () -> Unit, + onFastReconnect: suspend () -> Unit, ): SfuConnectionModule { return SfuConnectionModule( sfuUrl = sfuUrl, @@ -188,7 +188,7 @@ internal class SfuConnectionModule( scope: CoroutineScope = CoroutineScope(DispatcherProvider.IO), /** Network monitoring */ networkStateProvider: NetworkStateProvider, - onFastReconnect: () -> Unit, + onFastReconnect: suspend () -> Unit, ) { internal var sfuSocket: SfuSocket private val updatedSignalUrl = sfuUrl.removeSuffix(suffix = "/twirp") diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/PersistentSocket.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/PersistentSocket.kt index 7e2eafec45..790e08514d 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/PersistentSocket.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/PersistentSocket.kt @@ -63,7 +63,7 @@ public open class PersistentSocket( private val networkStateProvider: NetworkStateProvider, /** Set the scope everything should run in */ private val scope: CoroutineScope = CoroutineScope(DispatcherProvider.IO), - private val onFastReconnected: () -> Unit, + private val onFastReconnected: suspend () -> Unit, ) : WebSocketListener() { internal open val logger by taggedLogger("PersistentSocket") @@ -284,7 +284,7 @@ public open class PersistentSocket( } } - internal fun isPermanentError(error: Throwable): Boolean { + internal open fun isPermanentError(error: Throwable): Boolean { // errors returned by the server can be permanent. IE an invalid API call // or an expired token (required a refresh) // or temporary diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/SfuSocket.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/SfuSocket.kt index 4bda31a507..e1fd2fe82f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/SfuSocket.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/SfuSocket.kt @@ -44,6 +44,7 @@ import stream.video.sfu.event.SfuEvent import stream.video.sfu.event.SfuRequest import stream.video.sfu.models.ClientDetails import stream.video.sfu.models.Device +import stream.video.sfu.models.ErrorCode import stream.video.sfu.models.OS import stream.video.sfu.models.PeerType import stream.video.sfu.models.Sdk @@ -62,7 +63,7 @@ public class SfuSocket( private val scope: CoroutineScope = CoroutineScope(DispatcherProvider.IO), private val httpClient: OkHttpClient, private val networkStateProvider: NetworkStateProvider, - private val onFastReconnected: () -> Unit, + private val onFastReconnected: suspend () -> Unit, ) : PersistentSocket ( url = url, httpClient = httpClient, @@ -218,6 +219,16 @@ public class SfuSocket( } } + override fun isPermanentError(error: Throwable): Boolean { + if (error is SfuSocketError && + error.error?.code == ErrorCode.ERROR_CODE_PARTICIPANT_MEDIA_TRANSPORT_FAILURE + ) { + logger.w { "Received negotiation failure - disconnecting: $error" } + return false + } + return super.isPermanentError(error) + } + private suspend fun handleIceTrickle(event: ICETrickleEvent) { logger.d { "[handleIceTrickle] #sfu; #${event.peerType.stringify()}; candidate: ${event.candidate}"