Skip to content
Open
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 @@ -49,6 +49,7 @@ import io.getstream.video.android.util.StreamVideoInitHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
Expand All @@ -68,7 +69,9 @@ class CallActivity : ComposeStreamCallActivity() {
var observeCallReadyToJoinJob: Job? = null
var observeRingingJob: Job? = null
private val previousRingingStates = ConcurrentHashMap.newKeySet<RingingState>()
override val callJoinInterceptor = DemoCallJoinInterceptor(previousRingingStates)
private val coroutineScope = CoroutineScope(Dispatchers.Default)
override val callJoinInterceptor =
DemoCallJoinInterceptor(previousRingingStates, coroutineScope)

/**
* This code is required to pass the UI-tests (as it hardcodes the configuration)
Expand Down Expand Up @@ -207,6 +210,7 @@ class CallActivity : ComposeStreamCallActivity() {
super.finish()
observeCallReadyToJoinJob?.cancel()
observeRingingJob?.cancel()
coroutineScope.cancel()
previousRingingStates.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ import io.getstream.log.taggedLogger
import io.getstream.video.android.CallActivity.Companion.USE_CALL_JOIN_INTERCEPTOR
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.CallJoinInterceptor
import io.getstream.video.android.core.RealtimeConnection
import io.getstream.video.android.core.RingingState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.launch

/**
* Do the following changes before testing this flow
Expand All @@ -31,13 +39,44 @@ import kotlinx.coroutines.flow.first
*/
class DemoCallJoinInterceptor(
private val previousRingingStates: Set<RingingState>,
private val coroutineScope: CoroutineScope,
) : CallJoinInterceptor {
private val logger by taggedLogger("DemoCallJoinInterceptor")

companion object {
const val CALLER_READY_TO_JOIN_EVENT_TYPE = "caller_ready_join"
const val CALLEE_READY_TO_JOIN_EVENT_TYPE = "callee_ready_join"
}
private var observeJob: Job? = null
override suspend fun callWillJoin(call: Call) {
if (USE_CALL_JOIN_INTERCEPTOR) {
observeJob?.cancel()
observeJob = coroutineScope.launch {
val muteJob = launch {
call.state.participants
.flatMapLatest { participants ->
participants
.filter { !it.isLocal }
.asFlow()
.flatMapMerge { participant ->
participant.audioTrack.filterNotNull()
}
}
.collect { track ->
track.audio.setEnabled(false)
}
}
// Stop muting once the call reaches a terminal state, even if callDidJoin
// never runs (e.g. the interceptor aborts the join or it's left externally).
call.state.connection.first {
it is RealtimeConnection.Disconnected ||
it is RealtimeConnection.Failed ||
it is RealtimeConnection.ReconnectingFailed
}
muteJob.cancel()
}
}
}

override suspend fun callReadyToJoin(call: Call) {
if (USE_CALL_JOIN_INTERCEPTOR) {
Expand Down Expand Up @@ -81,4 +120,16 @@ class DemoCallJoinInterceptor(
}
}
}

override suspend fun callDidJoin(call: Call) {
if (USE_CALL_JOIN_INTERCEPTOR) {
observeJob?.cancel()
observeJob = null
call.state.participants.value.filter { !it.isLocal }
.forEach {
val audioTrack = it.audioTrack.value
audioTrack?.audio?.setEnabled(true)
}
}
}
}
4 changes: 4 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 @@ -8989,7 +8989,11 @@ public final class io/getstream/video/android/core/CallJoinInterceptionException
}

public abstract interface class io/getstream/video/android/core/CallJoinInterceptor {
public fun callDidJoin (Lio/getstream/video/android/core/Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun callDidJoin$suspendImpl (Lio/getstream/video/android/core/CallJoinInterceptor;Lio/getstream/video/android/core/Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun callReadyToJoin (Lio/getstream/video/android/core/Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun callWillJoin (Lio/getstream/video/android/core/Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun callWillJoin$suspendImpl (Lio/getstream/video/android/core/CallJoinInterceptor;Lio/getstream/video/android/core/Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class io/getstream/video/android/core/CallKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ internal class ActiveStateGate(
val shouldProceed = invokeInterceptor(call, interceptor)
if (!isActive) return@launch

if (shouldProceed) onReady()
if (shouldProceed) {
Comment thread
aleksandar-apostolov marked this conversation as resolved.
onReady()
invokeCallDidJoin(call, interceptor)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
cancelInterceptorJob()
},
)
Expand All @@ -107,12 +110,25 @@ internal class ActiveStateGate(
val shouldProceed = invokeInterceptor(call, interceptor)
if (!isActive) return@launch

if (shouldProceed) onReady()
if (shouldProceed) {
onReady()
invokeCallDidJoin(call, interceptor)
}
cleanup()
},
)
}

private suspend fun invokeCallDidJoin(call: Call, interceptor: CallJoinInterceptor?) {
try {
interceptor?.callDidJoin(call)
} catch (ex: CancellationException) {
throw ex
} catch (ex: Exception) {
logger.e(ex) { "[callDidJoin] interceptor threw, proceeding" }
}
}

private suspend fun awaitPeerConnection(call: Call) {
val start = System.currentTimeMillis()
val result =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
import io.getstream.video.android.core.utils.toQueriedMembers
import io.getstream.video.android.model.User
import io.getstream.webrtc.android.ui.VideoTextureViewRenderer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -142,7 +143,7 @@
"fastReconnectDeadlineSeconds. This constant will be removed in a future release.",
level = DeprecationLevel.WARNING,
)
const val sfuReconnectTimeoutMillis = 30_000

Check warning on line 146 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8Xjs6Y22uNeXUdq6eB&open=AZ8Xjs6Y22uNeXUdq6eB&pullRequest=1736

/**
* Outcome of a single reconnect attempt. Each reconnect method returns one of
Expand Down Expand Up @@ -204,9 +205,9 @@

internal val scope = CoroutineScope(clientImpl.scope.coroutineContext + supervisorJob)

// Must be initialized before `state` — CallState → SortedParticipantsState
// launches a coroutine that reads `call.events` (leaking-this race).
val events = MutableSharedFlow<VideoEvent>(extraBufferCapacity = 150)

Check warning on line 210 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Don't expose mutable flow types.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8Xjs6Y22uNeXUdq6d_&open=AZ8Xjs6Y22uNeXUdq6d_&pullRequest=1736

/** The call state contains all state such as the participant list, reactions etc */
val state = CallState(client, this, user, scope)
Expand Down Expand Up @@ -273,8 +274,8 @@
*/
private var isDestroyed = false

/** Session handles all real time communication for video and audio */
internal val session: MutableStateFlow<RtcSession?> = MutableStateFlow(null)

Check warning on line 278 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Don't expose mutable flow types.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8Xjs6Y22uNeXUdq6eA&open=AZ8Xjs6Y22uNeXUdq6eA&pullRequest=1736

var sessionId = UUID.randomUUID().toString()
internal val unifiedSessionId = UUID.randomUUID().toString()
Expand Down Expand Up @@ -560,7 +561,7 @@
return response
}

suspend fun join(

Check failure on line 564 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8Xjs6Y22uNeXUdq6eF&open=AZ8Xjs6Y22uNeXUdq6eF&pullRequest=1736
create: Boolean = false,
createOptions: CreateCallOptions? = null,
ring: Boolean = false,
Expand Down Expand Up @@ -625,6 +626,15 @@
"is joined. MediaManager will not be initialised with server settings."
}
}

try {
callJoinInterceptor?.callWillJoin(this)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
logger.e(e) { "[callWillJoin] interceptor threw, proceeding" }
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
return result
}
if (result is Failure) {
Expand Down Expand Up @@ -969,7 +979,7 @@
* @param strategy the initial reconnection strategy requested by the caller.
* @param reason a human-readable reason for logging / tracing.
*/
internal suspend fun reconnect(

Check failure on line 982 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 31 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8Xjs6Y22uNeXUdq6eG&open=AZ8Xjs6Y22uNeXUdq6eG&pullRequest=1736
strategy: WebsocketReconnectStrategy,
reason: String,
) {
Expand Down Expand Up @@ -1972,7 +1982,7 @@
}

@VisibleForTesting
internal suspend fun joinRequest(

Check warning on line 1985 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function has 8 parameters, which is greater than the 7 authorized.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8Xjs6Y22uNeXUdq6d-&open=AZ8Xjs6Y22uNeXUdq6d-&pullRequest=1736
create: CreateCallOptions? = null,
location: String,
migratingFrom: String? = null,
Expand Down Expand Up @@ -2056,7 +2066,7 @@
/**
* Should outlive both the call scope and the service scope and needs to be executed in the client-level scope.
* Because the call scope or service scope may be cancelled or finished while the network request is still in flight
* TODO: Run this in clientImpl.scope internally

Check warning on line 2069 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8Xjs6Y22uNeXUdq6eC&open=AZ8Xjs6Y22uNeXUdq6eC&pullRequest=1736
*/
suspend fun reject(reason: RejectReason? = null): Result<RejectCallResponse> {
logger.d { "[reject] #ringing; rejectReason: $reason, call_id:$id" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,41 @@
import kotlin.jvm.Throws

/**
* Controls when a ringing call transitions to [RingingState.Active].
* Controls when a ringing call transitions to [RingingState.Active], and provides
* best-effort hooks around the join lifecycle.
*
* Implement this to insert custom logic (e.g. waiting for user confirmation) between
* the publisher peer connection becoming ready and the call going active.
* Has no effect on non-ringing joins (livestream, direct join).
*
* Order: [callWillJoin] → [callReadyToJoin] → [callDidJoin].
*/
public interface CallJoinInterceptor {

/**
* Called right after the [io.getstream.video.android.core.call.RtcSession] is created,
* before the call goes [RingingState.Active]. Exceptions are logged and swallowed —
* this hook is best-effort and never fails the join.
*/
public suspend fun callWillJoin(call: Call) {}

Check failure on line 38 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallJoinInterceptor.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this function is empty or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8h_PRCIULl_yOLv2Tk&open=AZ8h_PRCIULl_yOLv2Tk&pullRequest=1736

/**
* Called when the SDK is ready to transition to [RingingState.Active].
* Suspend here to delay the transition; return to allow it to proceed.
*
* Throw [CallJoinInterceptionException] to abort the join — the SDK will leave
* the call cleanly
*
* The SDK enforces a 5-second maximum — the transition proceeds automatically on timeout.
* The SDK enforces a 5-second maximum [INTERCEPTOR_TIMEOUT_MS] — the transition proceeds automatically on timeout.
*/
@Throws(CallJoinInterceptionException::class)
public suspend fun callReadyToJoin(call: Call)

/**
* Called once the call has transitioned to [RingingState.Active] (after [callReadyToJoin]).
* Exceptions are logged and swallowed — this hook is best-effort and never fails the join.
*/
public suspend fun callDidJoin(call: Call) {}

Check failure on line 56 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallJoinInterceptor.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this function is empty or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ8h_PRCIULl_yOLv2Tl&open=AZ8h_PRCIULl_yOLv2Tl&pullRequest=1736
}

/**
Expand Down
Loading