Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ fun UserRobot.assertThatCallIsEnded(): UserRobot {
return this
}

fun UserRobot.assertCallUiIsRendered(): UserRobot {
assertTrue(
"Call information",
CallPage.callInfoView.waitDisplayed(timeOutMillis = 30.seconds),
)
assertTrue("Hang-up control", CallPage.hangUpButton.waitDisplayed())
assertTrue("Video surface", CallPage.videoView.waitDisplayed())
return this
}

fun UserRobot.assertUserMicrophone(isEnabled: Boolean, videoCall: Boolean = true): UserRobot {
// The participant view icon updates slightly after the control toggle, so both
// checks poll instead of asserting the icon at the instant the toggle appears.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@

package io.getstream.video.android.tests

import io.getstream.video.android.core.Call
import io.getstream.video.android.core.StreamVideo
import io.getstream.video.android.robots.assertCallUiIsRendered
import io.getstream.video.android.robots.assertParticipantsCountOnCall
import io.getstream.video.android.robots.assertThatCallIsEnded
import io.getstream.video.android.uiautomator.seconds
import io.qameta.allure.kotlin.Allure.step
import io.qameta.allure.kotlin.AllureId
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.UUID

Expand Down Expand Up @@ -105,8 +112,9 @@ class CallLifecycleTests : StreamTestCase() {

@AllureId("7012")
@Test
fun testUserReentersTheCall() {
fun testUserReusesTheSameCallObjectAfterLeaving() {
val participants = 1
lateinit var reusableCall: Call

step("GIVEN user starts a call") {
userRobot.joinCall()
Expand All @@ -115,11 +123,44 @@ class CallLifecycleTests : StreamTestCase() {
participantRobot.joinCall(callId)
userRobot.waitForParticipantsOnCall(participants)
}
step("WHEN user re-enters the call as the same user") {
userRobot.endCall().joinCall(callId)
step("AND the call UI is rendered") {
userRobot.assertCallUiIsRendered()
reusableCall = requireNotNull(StreamVideo.instance().state.activeCall.value) {
"Expected the rendered call to be registered as the active call"
}
}
step("THEN there is one participant on the call") {
userRobot.assertParticipantsCountOnCall(participants)
step("WHEN user leaves the call") {
userRobot
.endCall()
.assertThatCallIsEnded()
}
step("AND the same Call object joins again") {
val rejoinResult = runBlocking {
withTimeout(60_000) {
reusableCall.join()
}
}

assertTrue(
"Expected the same Call object to rejoin successfully",
rejoinResult.isSuccess,
)
assertSame(reusableCall, StreamVideo.instance().state.activeCall.value)
}
step("AND the call screen is opened for the rejoined call") {
userRobot.joinCall(callId)
}
step("THEN the call UI is rendered with the participant") {
userRobot
.waitForParticipantsOnCall(participants)
.assertParticipantsCountOnCall(participants)
.assertCallUiIsRendered()
assertSame(reusableCall, StreamVideo.instance().state.activeCall.value)
}
step("AND user can leave the reused call") {
userRobot
.endCall()
.assertThatCallIsEnded()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9809,7 +9809,6 @@ public abstract interface class io/getstream/video/android/core/CallJoinIntercep
}

public final class io/getstream/video/android/core/CallState {
public fun <init> (Lio/getstream/video/android/core/StreamVideo;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Lkotlinx/coroutines/CoroutineScope;)V
public final fun clearParticipants ()V
public final fun getAcceptedBy ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getActiveSpeakers ()Lkotlinx/coroutines/flow/StateFlow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import io.getstream.log.taggedLogger
import io.getstream.result.Result
import io.getstream.video.android.core.analytics.call.CallAnalytics
import io.getstream.video.android.core.analytics.call.observer.model.JoinAnalyticsModel
import io.getstream.video.android.core.call.Restartable
import io.getstream.video.android.core.call.RtcSession
import io.getstream.video.android.core.call.audio.InputAudioFilter
import io.getstream.video.android.core.call.components.CallApiClient
Expand All @@ -65,6 +66,7 @@ import io.getstream.video.android.core.call.components.CallLifecycleManager
import io.getstream.video.android.core.call.components.CallMediaManager
import io.getstream.video.android.core.call.components.CallReconnector
import io.getstream.video.android.core.call.components.CallRenderer
import io.getstream.video.android.core.call.components.CallRestartCoordinator
import io.getstream.video.android.core.call.components.CallSessionManager
import io.getstream.video.android.core.call.components.CallStatsReporter
import io.getstream.video.android.core.call.components.ClientCallRegistry
Expand All @@ -73,7 +75,6 @@ import io.getstream.video.android.core.call.components.NoiseCancellationPolicy
import io.getstream.video.android.core.call.components.RtcSessionFactory
import io.getstream.video.android.core.call.components.SessionMonitor
import io.getstream.video.android.core.call.connection.StreamPeerConnectionFactory
import io.getstream.video.android.core.call.scope.ScopeProvider
import io.getstream.video.android.core.call.scope.ScopeProviderImpl
import io.getstream.video.android.core.call.video.VideoFilter
import io.getstream.video.android.core.closedcaptions.ClosedCaptionsSettings
Expand All @@ -90,8 +91,6 @@ import io.getstream.video.android.core.notifications.internal.telecom.TelecomCal
import io.getstream.video.android.core.recording.RecordingType
import io.getstream.video.android.core.ringing.RingJoinSource
import io.getstream.video.android.core.ringing.RingStatePoller
import io.getstream.video.android.core.socket.common.scope.ClientScope
import io.getstream.video.android.core.socket.common.scope.UserScope
import io.getstream.video.android.core.socket.sfu.state.SfuSocketState
import io.getstream.video.android.core.trace.PeerConnectionTraceKey
import io.getstream.video.android.core.utils.SerialProcessor
Expand All @@ -103,8 +102,6 @@ import io.getstream.webrtc.android.ui.VideoTextureViewRenderer
import io.getstream.webrtc.audio.JavaAudioDeviceModule.AudioSamples
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -140,13 +137,19 @@ public class Call(
val user: User,
) {
internal val clientImpl = client as StreamVideoClient
internal val scopeProvider: ScopeProvider = ScopeProviderImpl(clientImpl.scope)

internal val scopeProvider = ScopeProviderImpl(
coroutineScope = clientImpl.scope,
callCid = "$type:$id",
)

private val logger by taggedLogger("Call:$type:$id")
private val supervisorJob = SupervisorJob()
internal var powerManager: PowerManager? = null

internal val scope = CoroutineScope(clientImpl.scope.coroutineContext + supervisorJob)
internal val scope: CoroutineScope = object : CoroutineScope {
override val coroutineContext
get() = scopeProvider.callScope.coroutineContext
}

/** Delegate that owns the live RTC session state and reconnect bookkeeping. */
private val sessionManager = CallSessionManager()
Expand Down Expand Up @@ -267,7 +270,7 @@ public class Call(
val events: MutableSharedFlow<VideoEvent> = eventManager.events

/** The call state contains all state such as the participant list, reactions etc */
val state = CallState(client, this, user, scope)
val state = CallState(client, this, user, scopeProvider)

internal val callAnalytics =
CallAnalytics(
Expand Down Expand Up @@ -370,31 +373,47 @@ public class Call(
type = type,
id = id,
clientImpl = clientImpl,
scope = scope,
coroutineScope = scope,
state = state,
sessionManager = sessionManager,
eglBase = { eglBase },
mediaManagerFactory = mediaManagerFactory,
)

/** Restores persistent components after the previous Call generation has been cleaned up. */
private val restartCoordinator = CallRestartCoordinator(
scopeProvider = scopeProvider,
restartables = listOf(
sessionManager,
callAnalytics,
statsReporter,
Restartable {
state.restart()
Result.Success(Unit)
},
media,
),
onRestarted = {
clientImpl.onCallReused(this@Call)
autoOnApplied = false
noiseCancellationSignalPending = false
desiredNoiseCancellationEnabled = false
observeNoiseCancellationSignalTarget()
observeNoiseCancellationPolicy()
},
)

/** Delegate that owns leave / end / cleanup teardown and the destroyed flag. */
private val lifecycle: CallLifecycleManager = CallLifecycleManager(
clientImpl = clientImpl,
sessionManager = sessionManager,
scopeProvider = scopeProvider,
callRegistry = callRegistry,
// Lets the REST calls queued before leave finish before the scope is torn down.
shutDownJobs = {
UserScope(ClientScope()).launch {
supervisorJob.children.forEach { it.join() }
supervisorJob.cancel()
}
scope.cancel()
// Cancelling the scope already reaches the poller, which runs on the call's job.
// Stopping it explicitly also clears the job reference, so a call torn down and
// re-armed does not see a cancelled job as a running one.
stopRingStatePolling()
_ringStatePoller = null
scopeProvider.cleanup()
},
restartCoordinator = restartCoordinator,
state = state,
callAnalytics = callAnalytics,
statsReporter = statsReporter,
Expand Down Expand Up @@ -612,6 +631,7 @@ public class Call(

/**
* Joins the call. Concurrent callers share one in-flight attempt.
* The same instance can be joined again after [leave]; pending cleanup finishes first.
*
* Cancelling this coroutine does not abort that attempt — only [leave] does.
*
Expand Down
Loading
Loading